From 44ab046ae5e5194524590005b3183f363e28aa85 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 23 Sep 2026 20:30:10 +0000 Subject: [PATCH 1/6] drop type assertions the checker already proves --- services/api/scripts/refresh-boundaries.ts | 2 +- services/api/src/adapters/push-fcm.ts | 2 +- services/api/src/auth/session-service.ts | 4 +- services/api/src/errors/glitchtip.ts | 2 +- services/api/src/routes/admin/_route-utils.ts | 4 +- .../api/src/routes/admin/discovery.routes.ts | 3 +- .../src/routes/admin/jurisdictions.routes.ts | 7 +- services/api/src/routes/host/_host-routes.ts | 4 +- services/api/src/routes/messages.routes.ts | 4 +- .../admin/gov-claims-repository.drizzle.ts | 2 +- .../admin/inbound-repository.drizzle.ts | 4 +- .../admin/jurisdiction-contacts-service.ts | 11 +--- .../services/admin/mail-repository.drizzle.ts | 2 +- .../admin/moderation-repository.drizzle.ts | 4 +- .../src/services/anon-repository.drizzle.ts | 2 +- .../api/src/services/certificate-model.ts | 4 +- .../src/services/chat-repository.drizzle.ts | 2 +- .../services/cleanup-repository.drizzle.ts | 12 ++-- .../api/src/services/dm-repository.drizzle.ts | 2 +- .../services/host/broadcast-email-sender.ts | 4 +- .../host/broadcast-repository.drizzle.ts | 4 +- .../src/services/host/broadcast-service.ts | 4 +- .../api/src/services/host/comms-wiring.ts | 2 +- .../host/export-repository.drizzle.ts | 2 +- .../api/src/services/host/export-service.ts | 2 +- .../registration-repository-pages.drizzle.ts | 5 +- .../src/services/host/registration-wiring.ts | 4 +- .../message-reactions-repository.drizzle.ts | 4 +- .../notification-repository.drizzle.ts | 4 +- .../src/services/report-repository.drizzle.ts | 2 +- services/api/test/helpers/auth.ts | 2 +- .../host/host-team-repository.memory.ts | 12 +--- services/api/test/helpers/pg-container.ts | 2 +- services/api/test/helpers/pg.ts | 4 +- .../integration/admin-mail-repository.test.ts | 2 +- .../anon-claim-code-security-pg.test.ts | 4 +- .../test/integration/chat-groups-pg.test.ts | 8 +-- .../integration/cleanup-read-ack-pg.test.ts | 1 + .../test/integration/cleanups-chat-pg.test.ts | 4 +- .../integration/host-page-media-pg.test.ts | 2 +- .../moderation-destination-refs-pg.test.ts | 5 +- .../test/integration/perf-indexes-pg.test.ts | 2 +- .../test/integration/signup-seats-pg.test.ts | 2 +- .../threads-lateral-bound-pg.test.ts | 2 +- .../test/unit/abuse-checks-adapter.test.ts | 2 +- .../api/test/unit/adapters-http-fetch.test.ts | 12 ++-- .../test/unit/adapters-i18n-messages.test.ts | 2 +- .../api/test/unit/adapters-mailer-oci.test.ts | 2 +- .../unit/admin-host-platform-routes.test.ts | 42 ++++++------ services/api/test/unit/admin-inbox.test.ts | 2 +- .../api/test/unit/admin-report-chat.test.ts | 2 +- .../api/test/unit/auth-oauth-security.test.ts | 2 +- .../api/test/unit/auth-route-mounting.test.ts | 4 +- services/api/test/unit/auth-routes.test.ts | 4 +- .../api/test/unit/certificate-routes.test.ts | 12 ++-- .../test/unit/certificate-verify-url.test.ts | 4 +- .../api/test/unit/chat-block-gates.test.ts | 16 ++--- .../unit/chat-broadcast-viewer-fields.test.ts | 7 +- .../test/unit/chat-mention-resolver.test.ts | 2 +- .../api/test/unit/chat-poll-notifier.test.ts | 3 +- .../api/test/unit/chat-pubsub-redis.test.ts | 6 +- .../unit/chat-room-notifier-wiring.test.ts | 13 ++-- .../unit/city-forward-logger-wiring.test.ts | 15 +++-- .../test/unit/city-forward-logging.test.ts | 2 +- .../api/test/unit/city-forward-wiring.test.ts | 15 +++-- .../unit/cleanup-service-security.test.ts | 3 +- .../test/unit/cleanup-slots-routes.test.ts | 6 +- .../api/test/unit/cleanup-slots-sql.test.ts | 3 +- .../unit/cleanup-status-derivation.test.ts | 1 + .../api/test/unit/cleanups-routes.test.ts | 12 ++-- .../conversations-routes-security.test.ts | 2 +- .../test/unit/counter-store-release.test.ts | 2 +- .../api/test/unit/feed-ranked-service.test.ts | 2 +- services/api/test/unit/feed-ranking.test.ts | 4 +- .../api/test/unit/glitchtip-scrub.test.ts | 4 +- .../unit/guest-rsvp-notice-optout.test.ts | 8 +-- .../unit/guest-rsvp-refusal-subcodes.test.ts | 4 +- .../api/test/unit/guest-rsvp-refusal.test.ts | 6 +- .../api/test/unit/guest-rsvp-security.test.ts | 4 +- .../api/test/unit/guest-rsvp-service.test.ts | 34 +++++----- .../held-media-moderation-reporter.test.ts | 6 +- .../test/unit/host-broadcast-audience.test.ts | 5 +- .../api/test/unit/host-export-csv.test.ts | 2 +- .../unit/host-keyset-cursor-precision.test.ts | 2 +- .../api/test/unit/host-team-seat-sql.test.ts | 3 +- .../unit/host/cleanup-host-fields.test.ts | 2 +- .../api/test/unit/host/host-routes.test.ts | 2 +- .../unit/host/hosted-event-ids-sql.test.ts | 7 +- .../test/unit/host/org-team-routes.test.ts | 22 +++---- .../host/organization-access-gate.test.ts | 14 ++-- .../organization-service-security.test.ts | 5 +- .../unit/host/organization-service.test.ts | 2 +- services/api/test/unit/jobs-pgboss.test.ts | 2 +- .../jurisdiction-contacts-upsert-sql.test.ts | 3 +- .../test/unit/local-storage-routes.test.ts | 4 +- .../test/unit/media-byte-quota-window.test.ts | 2 +- .../media-guest-upload-after-sign-in.test.ts | 2 +- .../test/unit/pg-stores-handle-race.test.ts | 2 +- .../api/test/unit/pg-stores-security.test.ts | 2 +- .../api/test/unit/redis-incr-window.test.ts | 2 +- .../unit/registration-questions-sql.test.ts | 15 ++--- .../api/test/unit/registration-sql.test.ts | 3 +- .../test/unit/report-chat-bell-wiring.test.ts | 17 ++--- .../unit/report-chat-emitter-seams.test.ts | 1 + .../unit/report-chat-fanout-wiring.test.ts | 1 + .../test/unit/report-chat-membership.test.ts | 12 ++-- .../report-service-replay-and-meta.test.ts | 2 +- .../test/unit/request-text-hardening.test.ts | 2 +- .../test/unit/reverse-geocode-mapbox.test.ts | 18 ++--- .../test/unit/reverse-geocode-photon.test.ts | 4 +- services/api/test/unit/route-coverage.test.ts | 4 +- .../api/test/unit/social-mentions.test.ts | 2 +- services/api/test/unit/social-routes.test.ts | 4 +- .../unit/socket-lifecycle-security.test.ts | 2 +- services/api/test/unit/storage-list.test.ts | 2 +- .../api/test/unit/storage-r2-proxy.test.ts | 8 +-- .../api/test/unit/thread-read-routes.test.ts | 14 ++-- .../test/unit/user-channel-gateway.test.ts | 4 +- .../test/unit/users-routes-security.test.ts | 2 +- services/api/test/unit/version-gate.test.ts | 10 +-- .../test/unit/volunteer-hours-service.test.ts | 6 +- .../test/unit/web-base-url-fallback.test.ts | 4 +- .../test/unit/ws-security-hardening.test.ts | 2 +- .../api/test/unit/ws-socket-lifecycle.test.ts | 65 +++++++++---------- services/media-worker/src/worker.ts | 2 +- services/media-worker/test/helpers/pg.ts | 4 +- .../retention-and-phash-pg.test.ts | 2 +- .../test/unit/egress-proxy.test.ts | 4 +- .../media-worker/test/unit/image-lane.test.ts | 2 +- .../test/unit/preflight-proof.test.ts | 6 +- .../test/unit/sandbox-hardening.test.ts | 36 +++++----- .../media-worker/test/unit/video-caps.test.ts | 1 + .../media-worker/test/unit/worker.test.ts | 8 +-- 133 files changed, 367 insertions(+), 423 deletions(-) diff --git a/services/api/scripts/refresh-boundaries.ts b/services/api/scripts/refresh-boundaries.ts index 3e1a1121..476c21b7 100644 --- a/services/api/scripts/refresh-boundaries.ts +++ b/services/api/scripts/refresh-boundaries.ts @@ -242,7 +242,7 @@ async function stampVintage( ): Promise { await sql` INSERT INTO boundary_vintage (id, vintage_tag, tiger_vintage, padus_version, row_counts, loaded_at) - VALUES (true, ${tag}, ${year}, ${PADUS_VERSION}, ${sql.json(rowCounts as Parameters[0])}, now()) + VALUES (true, ${tag}, ${year}, ${PADUS_VERSION}, ${sql.json(rowCounts)}, now()) ON CONFLICT (id) DO UPDATE SET vintage_tag = EXCLUDED.vintage_tag, tiger_vintage = EXCLUDED.tiger_vintage, diff --git a/services/api/src/adapters/push-fcm.ts b/services/api/src/adapters/push-fcm.ts index f2c5d7b1..f05c980d 100644 --- a/services/api/src/adapters/push-fcm.ts +++ b/services/api/src/adapters/push-fcm.ts @@ -65,7 +65,7 @@ export function makeFcmDispatcher( existing ?? admin.initializeApp( { - credential: admin.cert(serviceAccount as never), + credential: admin.cert(serviceAccount), ...(fcm.projectId !== undefined ? { projectId: fcm.projectId } : {}), }, FCM_APP_NAME, diff --git a/services/api/src/auth/session-service.ts b/services/api/src/auth/session-service.ts index be699a07..d51d86e4 100644 --- a/services/api/src/auth/session-service.ts +++ b/services/api/src/auth/session-service.ts @@ -421,11 +421,11 @@ export class SessionService { ) { return { userId: parsed.userId, - roles: parsed.roles as Role[], + roles: parsed.roles, expiresAtMs: parsed.expiresAtMs, createdAtMs: parsed.createdAtMs, epoch: parsed.epoch, - accountStatus: parsed.accountStatus as AccountStatus, + accountStatus: parsed.accountStatus, } } return null diff --git a/services/api/src/errors/glitchtip.ts b/services/api/src/errors/glitchtip.ts index 886aa91e..26fb3843 100644 --- a/services/api/src/errors/glitchtip.ts +++ b/services/api/src/errors/glitchtip.ts @@ -205,7 +205,7 @@ export async function initErrorReporting(opts: ErrorReportingOptions): Promise( ): () => S { return () => { const overrides = app[key] - return overrides === undefined - ? fromContainer() - : fromOverrides(overrides as NonNullable) + return overrides === undefined ? fromContainer() : fromOverrides(overrides) } } diff --git a/services/api/src/routes/admin/discovery.routes.ts b/services/api/src/routes/admin/discovery.routes.ts index 5b9123cf..0c357ab2 100644 --- a/services/api/src/routes/admin/discovery.routes.ts +++ b/services/api/src/routes/admin/discovery.routes.ts @@ -8,7 +8,6 @@ import { SaveDraftRequestSchema, type DiscoveryListResponse, type DiscoveryTaskDetailDTO, - type ReportCategory, } from "@civfix/shared" import type { FastifyInstance } from "fastify" import type { Container } from "../../di.js" @@ -90,7 +89,7 @@ export async function registerAdminDiscoveryRoutes( const actorId = requireOperator(request) const { id, body } = parseBodyWithId(SaveDraftRequestSchema, request) await service().saveDraft(id, { - contacts: (body.contacts ?? {}) as Partial>, + contacts: body.contacts ?? {}, defaultEmails: body.defaultEmails ?? [], // The shared `.url()` schema lets javascript:/data: URIs through. formUrl: httpUrlField(body.formUrl, "formUrl"), diff --git a/services/api/src/routes/admin/jurisdictions.routes.ts b/services/api/src/routes/admin/jurisdictions.routes.ts index f360f700..70e297a3 100644 --- a/services/api/src/routes/admin/jurisdictions.routes.ts +++ b/services/api/src/routes/admin/jurisdictions.routes.ts @@ -8,7 +8,6 @@ import { SaveContactsRequestSchema, type JurisdictionDirectoryResponse, type JurisdictionGeometryResponse, - type ReportCategory, } from "@civfix/shared" import type { FastifyInstance } from "fastify" import type { Container } from "../../di.js" @@ -80,7 +79,7 @@ export async function registerAdminJurisdictionsRoutes( await service().saveAndRoute( geoid, { - contacts: (body.contacts ?? {}) as Partial>, + contacts: body.contacts ?? {}, defaultEmails: body.defaultEmails ?? [], // The shared `.url()` schema lets javascript:/data: URIs through. formUrl: httpUrlField(body.formUrl, "formUrl"), @@ -115,9 +114,7 @@ export async function registerAdminJurisdictionsRoutes( await service().patch( geoid, { - ...(body.contacts !== undefined - ? { contacts: body.contacts as Partial> } - : {}), + ...(body.contacts !== undefined ? { contacts: body.contacts } : {}), ...(body.defaultEmails !== undefined ? { defaultEmails: body.defaultEmails } : {}), // Same scheme allowlist as save; an absent field still means "leave unchanged". ...(body.formUrl !== undefined ? { formUrl: httpUrlField(body.formUrl, "formUrl") } : {}), diff --git a/services/api/src/routes/host/_host-routes.ts b/services/api/src/routes/host/_host-routes.ts index 9cdc8c09..30f9222c 100644 --- a/services/api/src/routes/host/_host-routes.ts +++ b/services/api/src/routes/host/_host-routes.ts @@ -93,9 +93,9 @@ export function makeHostRouteContext(app: FastifyInstance, container: Container) } export function bodyWith(request: FastifyRequest, extra: Record): object { - return { ...((request.body ?? {}) as object), ...extra } + return { ...(request.body ?? {}), ...extra } } export function queryWith(request: FastifyRequest, extra: Record): object { - return { ...((request.query ?? {}) as object), ...extra } + return { ...(request.query ?? {}), ...extra } } diff --git a/services/api/src/routes/messages.routes.ts b/services/api/src/routes/messages.routes.ts index aa7d4841..3f54bd7f 100644 --- a/services/api/src/routes/messages.routes.ts +++ b/services/api/src/routes/messages.routes.ts @@ -43,7 +43,7 @@ import { chatMentionDeps, type ChatMentionSeam } from "./chat-gateway-wiring.js" import { broadcastMessageUpdate, roomKeyFor } from "../ws/gateway.js" import { wireChatPowers } from "./chat-powers-wiring.js" import { makeChatPollRepository } from "../services/chat-poll-repository.drizzle.js" -import { makeChatPollService, type PollRoomKind } from "../services/chat-poll-service.js" +import { makeChatPollService } from "../services/chat-poll-service.js" import { makeContainerPollNotifier } from "../services/chat-poll-notifier.js" export const EditMessageBodySchema = trimTextFields(EditMessageRequestSchema, "body") @@ -386,7 +386,7 @@ export async function registerMessagesRoutes( const userId = requireAuth(request) const body = parse(CreatePollBodySchema, request.body) const created = await getPollService().createPoll({ - roomKind: body.roomKind as PollRoomKind, + roomKind: body.roomKind, roomId: body.roomId, question: body.question, options: body.options, diff --git a/services/api/src/services/admin/gov-claims-repository.drizzle.ts b/services/api/src/services/admin/gov-claims-repository.drizzle.ts index 7a9b5b48..0ffff815 100644 --- a/services/api/src/services/admin/gov-claims-repository.drizzle.ts +++ b/services/api/src/services/admin/gov-claims-repository.drizzle.ts @@ -157,7 +157,7 @@ export function makeDrizzleGovClaimsRepository(sql: Sql): GovClaimsRepository { UPDATE gov_claims SET checks = COALESCE(checks, '{}'::jsonb) || jsonb_build_object( ${input.check}::text, - ${tx.json(checkValue as Parameters[0])} + ${tx.json(checkValue)} ) WHERE id = ${id} AND status = 'pending' RETURNING ${cols} diff --git a/services/api/src/services/admin/inbound-repository.drizzle.ts b/services/api/src/services/admin/inbound-repository.drizzle.ts index c2cfa356..06d22811 100644 --- a/services/api/src/services/admin/inbound-repository.drizzle.ts +++ b/services/api/src/services/admin/inbound-repository.drizzle.ts @@ -87,8 +87,8 @@ export function makeDrizzleInboundRepository(sql: Sql): InboundRepository { ${input.subject}, ${input.bodyText}, ${input.bodyHtml}, - ${sql.json(input.headers as Parameters[0])}, - ${sql.json(input.attachments as Parameters[0])}, + ${sql.json(input.headers)}, + ${sql.json(input.attachments)}, ${input.attachments.length > 0}, ${receivedAt} ) diff --git a/services/api/src/services/admin/jurisdiction-contacts-service.ts b/services/api/src/services/admin/jurisdiction-contacts-service.ts index 34a2feb9..f1246ba7 100644 --- a/services/api/src/services/admin/jurisdiction-contacts-service.ts +++ b/services/api/src/services/admin/jurisdiction-contacts-service.ts @@ -4,12 +4,7 @@ import { AppError } from "@civfix/shared" import type { JurisdictionListQuery } from "@civfix/shared" import { toDirectoryDTO, hasAnyContact } from "./jurisdiction-directory-projection.js" -import type { - DirectoryFilter, - DirectorySort, - PatchContactsInput, - SaveContactsInput, -} from "./jurisdiction-contacts-repository.js" +import type { PatchContactsInput, SaveContactsInput } from "./jurisdiction-contacts-repository.js" import { isReservedHandle } from "../../auth/reserved-handles.js" import { clampLimit } from "./pagination.js" import { isThrottled } from "./outreach-service.js" @@ -67,9 +62,9 @@ export function makeJurisdictionContactsService( // The shared Zod enums enforced at the wire boundary are structurally the ListDirectoryArgs unions. const { records, nextCursor, total, facets } = await deps.repo.listDirectory({ q: query.q && query.q.trim() !== "" ? query.q.trim() : null, - filter: (query.filter ?? "all") as DirectoryFilter, + filter: query.filter ?? "all", layer: query.layer ?? null, - sort: (query.sort ?? "population") as DirectorySort, + sort: query.sort ?? "population", cursor: query.cursor ?? null, limit: clampLimit(query.limit), }) diff --git a/services/api/src/services/admin/mail-repository.drizzle.ts b/services/api/src/services/admin/mail-repository.drizzle.ts index 3ec58e7d..ff3270c6 100644 --- a/services/api/src/services/admin/mail-repository.drizzle.ts +++ b/services/api/src/services/admin/mail-repository.drizzle.ts @@ -438,7 +438,7 @@ export function makeDrizzleMailRepository(sql: Sql): MailRepository { ${input.body ?? null}, ${input.html ?? null}, ${input.kind ?? null}, - ${tx.json(attachments as Parameters[0])}, + ${tx.json(attachments)}, ${input.messageId ?? null}, ${input.inReplyTo ?? null}, ${input.unaffiliated ?? false} diff --git a/services/api/src/services/admin/moderation-repository.drizzle.ts b/services/api/src/services/admin/moderation-repository.drizzle.ts index 89d75a5b..b5c89d78 100644 --- a/services/api/src/services/admin/moderation-repository.drizzle.ts +++ b/services/api/src/services/admin/moderation-repository.drizzle.ts @@ -936,8 +936,8 @@ export async function insertModerationItem( ${input.place ?? null}, ${input.priority ?? DEFAULT_ITEM_PRIORITY}, ${input.autoAction ?? null}, - ${tx.json((input.signals ?? []) as Parameters[0])}, - ${tx.json((input.similar ?? []) as Parameters[0])}, + ${tx.json(input.signals ?? [])}, + ${tx.json(input.similar ?? [])}, ${"open"}, ${tx.json(meta as Parameters[0])} ) diff --git a/services/api/src/services/anon-repository.drizzle.ts b/services/api/src/services/anon-repository.drizzle.ts index 31c00ef1..cad6dd39 100644 --- a/services/api/src/services/anon-repository.drizzle.ts +++ b/services/api/src/services/anon-repository.drizzle.ts @@ -347,7 +347,7 @@ async function persistIdempotencySnapshot( ${args.idempotencyKey}, ${ANON_REPORT_CREATE_SCOPE}, ${args.anonSessionId}, - ${sql.json(storedSnapshot as Parameters[0])} + ${sql.json(storedSnapshot)} ) ` } diff --git a/services/api/src/services/certificate-model.ts b/services/api/src/services/certificate-model.ts index cadb598b..ed2d5101 100644 --- a/services/api/src/services/certificate-model.ts +++ b/services/api/src/services/certificate-model.ts @@ -2,7 +2,7 @@ import { MAX_CERTIFICATE_ENTRIES } from "@civfix/shared" import type { VolunteerHoursSource } from "@civfix/shared" import { resolveLocale } from "../i18n/locales.js" import { sha256HexSync } from "../lib/hash.js" -import { renderMessage, type MessageKey } from "../i18n/renderMessage.js" +import { renderMessage } from "../i18n/renderMessage.js" export const CERTIFICATE_TIME_ZONE = "America/Los_Angeles" @@ -47,7 +47,7 @@ export type CertificateTranslator = ( export function certificateTranslator(locale: unknown): CertificateTranslator { const resolved = resolveLocale(locale) - return (key, vars) => renderMessage(resolved, key as unknown as MessageKey, vars) + return (key, vars) => renderMessage(resolved, key, vars) } export interface TranscriptLedgerRow { diff --git a/services/api/src/services/chat-repository.drizzle.ts b/services/api/src/services/chat-repository.drizzle.ts index cd206469..73812b04 100644 --- a/services/api/src/services/chat-repository.drizzle.ts +++ b/services/api/src/services/chat-repository.drizzle.ts @@ -440,7 +440,7 @@ export function makeDrizzleChatRepository(sql: Sql, presign?: PresignMedia): Cha : Promise.resolve(null), ]) const attachments = wantsMedia - ? ((await loadChatAttachments(sql, [id], presign!, input.userId)).get(id) ?? []) + ? ((await loadChatAttachments(sql, [id], presign, input.userId)).get(id) ?? []) : [] return toMessageDTO(rows[0]!, { viewerUserId: input.userId, diff --git a/services/api/src/services/cleanup-repository.drizzle.ts b/services/api/src/services/cleanup-repository.drizzle.ts index e5d678b1..8b84b4b3 100644 --- a/services/api/src/services/cleanup-repository.drizzle.ts +++ b/services/api/src/services/cleanup-repository.drizzle.ts @@ -375,7 +375,7 @@ function hostSetFragments(sql: Queryable, patch: EventHostWrite): SqlFragment[] if (patch.visibility !== undefined) sets.push(sql`visibility = ${patch.visibility}`) if (patch.coverMediaId !== undefined) sets.push(sql`cover_media_id = ${patch.coverMediaId}`) if (patch.galleryMediaIds !== undefined) { - sets.push(sql`gallery_media_ids = ${patch.galleryMediaIds as unknown as string[]}`) + sets.push(sql`gallery_media_ids = ${patch.galleryMediaIds}`) } if (patch.donationUrl !== undefined) sets.push(sql`donation_url = ${patch.donationUrl}`) if (patch.pageSlug !== undefined) sets.push(sql`page_slug = ${patch.pageSlug}`) @@ -389,7 +389,7 @@ function hostSetFragments(sql: Queryable, patch: EventHostWrite): SqlFragment[] sets.push(sql`organization_id = ${patch.organizationId}`) } if (patch.reminderOffsetsMin !== undefined) { - sets.push(sql`reminder_offsets_min = ${patch.reminderOffsetsMin as unknown as number[] | null}`) + sets.push(sql`reminder_offsets_min = ${patch.reminderOffsetsMin}`) } if (patch.hostReplyTo !== undefined) { sets.push(sql`host_reply_to = ${patch.hostReplyTo}, host_reply_to_verified_at = NULL`) @@ -412,7 +412,7 @@ function cleanupSetList(sql: Queryable, patch: UpdateCleanupPatch): SqlFragment sets.push(sql`address_source = ${patch.addressSource}`) } if (patch.bring !== undefined) { - sets.push(sql`bring = ${patch.bring as unknown as string[] | null}`) + sets.push(sql`bring = ${patch.bring}`) } if (patch.jurisdictionGeoid !== undefined) { sets.push(sql`jurisdiction_geoid = ${patch.jurisdictionGeoid}`) @@ -529,7 +529,7 @@ export function makeDrizzleCleanupRepository(sql: Sql): CleanupRepository { ST_SetSRID(ST_MakePoint(${args.lng}, ${args.lat}), 4326), ${args.scheduledAt}, ${args.status}, - ${args.bring as unknown as string[] | null}, + ${args.bring}, ${args.address}, ${args.addressSource}, ${args.jurisdictionGeoid}, @@ -538,13 +538,13 @@ export function makeDrizzleCleanupRepository(sql: Sql): CleanupRepository { ${args.host.timezone ?? null}, ${args.host.visibility ?? "public"}, ${args.host.coverMediaId ?? null}, - ${(args.host.galleryMediaIds ?? []) as unknown as string[]}, + ${args.host.galleryMediaIds ?? []}, ${args.host.donationUrl ?? null}, ${args.host.pageSlug ?? null}, ${args.host.registrationOpensAt ?? null}, ${args.host.registrationClosesAt ?? null}, ${args.host.organizationId ?? null}, - ${(args.host.reminderOffsetsMin ?? null) as unknown as number[] | null}, + ${args.host.reminderOffsetsMin ?? null}, ${args.host.hostReplyTo ?? null} ) ` diff --git a/services/api/src/services/dm-repository.drizzle.ts b/services/api/src/services/dm-repository.drizzle.ts index 27c65e4c..6beee26d 100644 --- a/services/api/src/services/dm-repository.drizzle.ts +++ b/services/api/src/services/dm-repository.drizzle.ts @@ -259,7 +259,7 @@ export function makeDrizzleDmRepository(sql: Sql, presign?: PresignMedia): DmRep : await run(sql) const messageId = rows[0]!.id const attachments = wantsMedia - ? ((await loadChatAttachments(sql, [messageId], presign!, input.senderId)).get(messageId) ?? + ? ((await loadChatAttachments(sql, [messageId], presign, input.senderId)).get(messageId) ?? []) : [] return toMessageDTO(rows[0]!, { diff --git a/services/api/src/services/host/broadcast-email-sender.ts b/services/api/src/services/host/broadcast-email-sender.ts index 554c7366..fce7f9ce 100644 --- a/services/api/src/services/host/broadcast-email-sender.ts +++ b/services/api/src/services/host/broadcast-email-sender.ts @@ -1,4 +1,4 @@ -import type { BroadcastKind, DeliveryFailureKind } from "@civfix/shared" +import type { BroadcastKind } from "@civfix/shared" import type { BroadcastVarValues } from "@civfix/shared/host" import type { Mailer } from "@civfix/shared/interfaces" import type { FastifyBaseLogger } from "fastify" @@ -308,7 +308,7 @@ export function makeBroadcastEmailSender(deps: BroadcastEmailSenderDeps) { chunk.outcomes.push({ id: claim.id, status: "failed", - failureKind: failure.kind as DeliveryFailureKind, + failureKind: failure.kind, }) } diff --git a/services/api/src/services/host/broadcast-repository.drizzle.ts b/services/api/src/services/host/broadcast-repository.drizzle.ts index 2f3a628e..710bae77 100644 --- a/services/api/src/services/host/broadcast-repository.drizzle.ts +++ b/services/api/src/services/host/broadcast-repository.drizzle.ts @@ -223,7 +223,7 @@ export function makeDrizzleBroadcastRepository(sql: Sql): BroadcastRepository { AND created_at >= ${cap.since}` if ((rows[0]?.n ?? 0) >= cap.max) return null return insertBroadcast(tx, input) - }) as Promise + }) }, async createIfAbsent(input: BroadcastCreateInput): Promise { @@ -920,7 +920,7 @@ export function makeDrizzleBroadcastRepository(sql: Sql): BroadcastRepository { if (rows.length === 0) return false await insertAuditRow(tx, audit) return true - }) as Promise + }) }, async isEmailSuppressed(emailHash: string): Promise { diff --git a/services/api/src/services/host/broadcast-service.ts b/services/api/src/services/host/broadcast-service.ts index 34ac0954..cdb3c6a6 100644 --- a/services/api/src/services/host/broadcast-service.ts +++ b/services/api/src/services/host/broadcast-service.ts @@ -423,7 +423,7 @@ export function makeBroadcastService(deps: BroadcastServiceDeps): BroadcastServi ctaLabel: body.ctaLabel ?? null, ctaUrl: body.ctaUrl ?? null, segment: body.segment, - channels: body.channels as BroadcastChannel[], + channels: body.channels, status: "draft", chunkSize: config.chunkSize, }) @@ -447,7 +447,7 @@ export function makeBroadcastService(deps: BroadcastServiceDeps): BroadcastServi ...("ctaLabel" in body ? { ctaLabel: body.ctaLabel ?? null } : {}), ...("ctaUrl" in body ? { ctaUrl: body.ctaUrl ?? null } : {}), ...(body.segment !== undefined ? { segment: body.segment } : {}), - ...(body.channels !== undefined ? { channels: body.channels as BroadcastChannel[] } : {}), + ...(body.channels !== undefined ? { channels: body.channels } : {}), } let updated: BroadcastRecord | null = current.status === "draft" ? current : null if (Object.keys(patch).length > 0) { diff --git a/services/api/src/services/host/comms-wiring.ts b/services/api/src/services/host/comms-wiring.ts index 9a6e3c33..26989b83 100644 --- a/services/api/src/services/host/comms-wiring.ts +++ b/services/api/src/services/host/comms-wiring.ts @@ -160,7 +160,7 @@ export function makeCommsRuntime(container: Container, logger?: CommsLogger): Co const pipeline = makeBroadcastPipeline({ repo, service: broadcasts, - notifications: makeRouteNotificationService(container, logger as FastifyBaseLogger | undefined), + notifications: makeRouteNotificationService(container, logger), mailer: container.mailer, cache: container.getCache(), config, diff --git a/services/api/src/services/host/export-repository.drizzle.ts b/services/api/src/services/host/export-repository.drizzle.ts index 15c3f0eb..3d9c79b8 100644 --- a/services/api/src/services/host/export-repository.drizzle.ts +++ b/services/api/src/services/host/export-repository.drizzle.ts @@ -68,7 +68,7 @@ export function makeDrizzleHostExportRepository(sql: Sql): HostExportRepository const record = toRecord(rows[0]!) if (audit !== undefined) await insertAuditRow(tx, audit(record.id)) return record - }) as Promise + }) }, async findById(exportId) { diff --git a/services/api/src/services/host/export-service.ts b/services/api/src/services/host/export-service.ts index fceb2b05..09fb78dc 100644 --- a/services/api/src/services/host/export-service.ts +++ b/services/api/src/services/host/export-service.ts @@ -256,7 +256,7 @@ export function makeHostExportService(deps: HostExportServiceDeps): HostExportSe organizationId: args.organizationId, requestedBy: args.requestedBy, kind: args.kind, - filters: (args.filters ?? {}) as Record, + filters: args.filters ?? {}, }, args.audit, ) diff --git a/services/api/src/services/host/registration-repository-pages.drizzle.ts b/services/api/src/services/host/registration-repository-pages.drizzle.ts index 3d33c0d9..e258ba50 100644 --- a/services/api/src/services/host/registration-repository-pages.drizzle.ts +++ b/services/api/src/services/host/registration-repository-pages.drizzle.ts @@ -104,9 +104,8 @@ async function syncPageMediaIn( } async function upsertPageRowIn(tx: TransactionSql, args: SavePageArgs): Promise { - const blocks = tx.json(args.blocks as unknown as Parameters[0]) - const seo = - args.seo === undefined ? null : tx.json(args.seo as unknown as Parameters[0]) + const blocks = tx.json(args.blocks) + const seo = args.seo === undefined ? null : tx.json(args.seo) await tx` INSERT INTO cleanup_pages ( cleanup_id, status, theme_accent, blocks, seo, created_at, updated_at diff --git a/services/api/src/services/host/registration-wiring.ts b/services/api/src/services/host/registration-wiring.ts index ced59e45..15c2e985 100644 --- a/services/api/src/services/host/registration-wiring.ts +++ b/services/api/src/services/host/registration-wiring.ts @@ -83,9 +83,7 @@ function platformMediaUrlPrefixes(container: Container): string[] { function lazyNotifier(container: Container, logger?: HostServiceLogger): RegistrationNotifier { return { createNotification: (userId, input) => - container - .getNotificationService(logger as Parameters[0]) - .createNotification(userId, input), + container.getNotificationService(logger).createNotification(userId, input), } } diff --git a/services/api/src/services/message-reactions-repository.drizzle.ts b/services/api/src/services/message-reactions-repository.drizzle.ts index 8bd6e7cb..08dfa529 100644 --- a/services/api/src/services/message-reactions-repository.drizzle.ts +++ b/services/api/src/services/message-reactions-repository.drizzle.ts @@ -1,5 +1,5 @@ import type { Queryable, Sql } from "../db/client.js" -import type { ReactionEmoji, ReactionSummaryDTO } from "@civfix/shared" +import type { ReactionSummaryDTO } from "@civfix/shared" import type { MessageReactionRepository } from "./message-reactions-repository.js" // `table` is a module-constant union literal, never user input, interpolated as a postgres.js identifier. @@ -58,7 +58,7 @@ export async function loadReactionsFor( ` for (const r of rows) { const dto: ReactionSummaryDTO = { - emoji: r.emoji as ReactionEmoji, + emoji: r.emoji, count: r.count, mine: viewerUserId !== null && r.mine, } diff --git a/services/api/src/services/notification-repository.drizzle.ts b/services/api/src/services/notification-repository.drizzle.ts index 1e796c34..8a303c1a 100644 --- a/services/api/src/services/notification-repository.drizzle.ts +++ b/services/api/src/services/notification-repository.drizzle.ts @@ -330,7 +330,7 @@ export function makeDrizzleNotificationRepository(sql: Sql): NotificationReposit RETURNING id, user_id, type, title, body, link, read_at, created_at ` return { record: toRecord(rows[0]!), deduped: false } - }) as Promise<{ record: NotificationRecord; deduped: boolean }> + }) }, refreshUnreadNotification(args: CoalescedNotificationArgs): Promise { @@ -356,7 +356,7 @@ export function makeDrizzleNotificationRepository(sql: Sql): NotificationReposit RETURNING id, user_id, type, title, body, link, read_at, created_at ` return { record: toRecord(rows[0]!), coalesced: false } - }) as Promise<{ record: NotificationRecord; coalesced: boolean }> + }) }, async deleteAllNotificationsForUser(userId: string): Promise { diff --git a/services/api/src/services/report-repository.drizzle.ts b/services/api/src/services/report-repository.drizzle.ts index b397516b..1e6254ef 100644 --- a/services/api/src/services/report-repository.drizzle.ts +++ b/services/api/src/services/report-repository.drizzle.ts @@ -279,7 +279,7 @@ export function makeDrizzleReportRepository(sql: Sql): ReportRepository { ${args.idempotency.key}, ${args.idempotency.scope}, ${args.idempotency.userOrAnon}, - ${sql.json(dto as Parameters[0])} + ${sql.json(dto)} ) ` return dto diff --git a/services/api/test/helpers/auth.ts b/services/api/test/helpers/auth.ts index 32c94c86..5cdcb5e7 100644 --- a/services/api/test/helpers/auth.ts +++ b/services/api/test/helpers/auth.ts @@ -92,7 +92,7 @@ export async function signIn( if (verified.statusCode !== 200) { throw new Error(`signIn(${email}): otp/verify ${verified.statusCode} ${verified.body}`) } - const body = verified.json() as SessionResponse + const body = verified.json() if (typeof body.token !== "string" || body.token === "") { throw new Error(`signIn(${email}): otp/verify returned no bearer token (${verified.body})`) } diff --git a/services/api/test/helpers/host/host-team-repository.memory.ts b/services/api/test/helpers/host/host-team-repository.memory.ts index b2f259e5..73f287b8 100644 --- a/services/api/test/helpers/host/host-team-repository.memory.ts +++ b/services/api/test/helpers/host/host-team-repository.memory.ts @@ -1,12 +1,6 @@ import { randomUUID } from "node:crypto" import { AppError, MAX_TEAM_INVITES_PER_EVENT } from "@civfix/shared" -import type { - CleanupMemberRole, - CleanupStatus, - EventTeamInviteStatus, - EventTeamRole, - EventVisibility, -} from "@civfix/shared" +import type { CleanupMemberRole, EventTeamInviteStatus, EventTeamRole } from "@civfix/shared" import { encodeTimeCursor, isBeforeTimeCursor, @@ -139,8 +133,8 @@ export class InMemoryHostTeamRepository implements HostTeamRepository { title: over.title ?? SEED_EVENT_TITLE, startsAt: over.startsAt ?? new Date(Date.now() + SEED_EVENT_LEAD_MS), endsAt: over.endsAt ?? null, - status: over.status ?? ("upcoming" as CleanupStatus), - visibility: over.visibility ?? ("public" as EventVisibility), + status: over.status ?? "upcoming", + visibility: over.visibility ?? "public", coverKey: over.coverKey ?? null, address: over.address ?? null, } diff --git a/services/api/test/helpers/pg-container.ts b/services/api/test/helpers/pg-container.ts index f5e0a83d..dd627b39 100644 --- a/services/api/test/helpers/pg-container.ts +++ b/services/api/test/helpers/pg-container.ts @@ -153,7 +153,7 @@ export async function startSharedPg(): Promise { const templateSql = postgres(uriWithDatabase(adminUri, TEMPLATE_DB), { max: 4, onnotice: () => {}, - }) as Sql + }) try { // Apply the EXACT canonical SQL the production runner applies, then the shared seed. await applyMigrations(templateSql, MIGRATIONS_DIR) diff --git a/services/api/test/helpers/pg.ts b/services/api/test/helpers/pg.ts index e8f0b7d3..ee750e1d 100644 --- a/services/api/test/helpers/pg.ts +++ b/services/api/test/helpers/pg.ts @@ -121,7 +121,7 @@ async function create(): Promise { */ function openHarness(uri: string, after: () => Promise): PgHarness { // Raw client (full postgres.js serialization) for the repositories and test-side inserts. - const sql = postgres(uri, { max: 4, onnotice: () => {} }) as Sql + const sql = postgres(uri, { max: 4, onnotice: () => {} }) // Drizzle gets its OWN client so it never clobbers `sql`'s value serializers (see makeDb in // src/db/client.ts and drizzle-orm#3108). const drizzleSql = postgres(uri, { max: 2, onnotice: () => {} }) @@ -176,7 +176,7 @@ async function bootOwnContainer(): Promise { */ function readProvided(): ProvidedPg | undefined { try { - return inject("civfixPg") as ProvidedPg | undefined + return inject("civfixPg") } catch { return undefined } diff --git a/services/api/test/integration/admin-mail-repository.test.ts b/services/api/test/integration/admin-mail-repository.test.ts index 2ad53a12..c57375f6 100644 --- a/services/api/test/integration/admin-mail-repository.test.ts +++ b/services/api/test/integration/admin-mail-repository.test.ts @@ -92,7 +92,7 @@ describe.skipIf(!pg)("admin mail repository (integration: real schema)", () => { const dto = await repo.getThread(t.id) const [small, huge] = dto!.messages expect(small!.truncated).toBeUndefined() - expect(huge!.body!.length).toBe(MAIL_BODY_DETAIL_CHARS) + expect(huge!.body.length).toBe(MAIL_BODY_DETAIL_CHARS) expect(huge!.truncated).toBe(true) expect(await repo.getLastOutboundRecipient(t.id)).toBe("clerk@city.gov") diff --git a/services/api/test/integration/anon-claim-code-security-pg.test.ts b/services/api/test/integration/anon-claim-code-security-pg.test.ts index a61bc8ad..19059458 100644 --- a/services/api/test/integration/anon-claim-code-security-pg.test.ts +++ b/services/api/test/integration/anon-claim-code-security-pg.test.ts @@ -85,7 +85,7 @@ describe.skipIf(!pg)("anonymous claim code at rest (integration)", () => { it("a replay hands back a fresh code that claims the report, and retires the first one", async () => { const key = randomUUID() const first = await anon.submitAnonReport(req(key), { ip: "203.0.113.41", cfGeo: {} }) - const replay = await anon.submitAnonReport(req(key, first.issuedAnonToken!), { + const replay = await anon.submitAnonReport(req(key, first.issuedAnonToken), { ip: "203.0.113.41", cfGeo: {}, }) @@ -111,7 +111,7 @@ describe.skipIf(!pg)("anonymous claim code at rest (integration)", () => { await claim.claimReport(first.response.claimCode, u!.id) await expect( - anon.submitAnonReport(req(key, first.issuedAnonToken!), { ip: "203.0.113.42", cfGeo: {} }), + anon.submitAnonReport(req(key, first.issuedAnonToken), { ip: "203.0.113.42", cfGeo: {} }), ).rejects.toMatchObject({ code: "CONFLICT" }) }) }) diff --git a/services/api/test/integration/chat-groups-pg.test.ts b/services/api/test/integration/chat-groups-pg.test.ts index 409e265f..39063147 100644 --- a/services/api/test/integration/chat-groups-pg.test.ts +++ b/services/api/test/integration/chat-groups-pg.test.ts @@ -435,10 +435,10 @@ describe.skipIf(!pg)("chat groups service + routes (integration)", () => { const tok = await token(ownerId) const page1 = await inject(tok, "GET", `/v1/groups/${dto.id}/members?limit=3`) expect(page1.statusCode).toBe(200) - const p1 = page1.json() as { + const p1 = page1.json<{ members: Array<{ user: { id: string }; role: string }> nextCursor: string | null - } + }>() expect(p1.members).toHaveLength(3) // Ordering: owner first, then members by joined_at ASC (insertion order within the create tx). expect(p1.members[0]).toMatchObject({ user: { id: ownerId }, role: "owner" }) @@ -450,10 +450,10 @@ describe.skipIf(!pg)("chat groups service + routes (integration)", () => { `/v1/groups/${dto.id}/members?limit=3&cursor=${p1.nextCursor}`, ) expect(page2.statusCode).toBe(200) - const p2 = page2.json() as { + const p2 = page2.json<{ members: Array<{ user: { id: string } }> nextCursor: string | null - } + }>() expect(p2.members).toHaveLength(3) expect(p2.nextCursor).toBeNull() diff --git a/services/api/test/integration/cleanup-read-ack-pg.test.ts b/services/api/test/integration/cleanup-read-ack-pg.test.ts index 03e1f6ea..275c35b0 100644 --- a/services/api/test/integration/cleanup-read-ack-pg.test.ts +++ b/services/api/test/integration/cleanup-read-ack-pg.test.ts @@ -23,6 +23,7 @@ import { makeDrizzleChatReadState } from "../../src/services/read-watermark-repo type MarkRead = (cleanupId: string, userId: string, upToId: string) => Promise +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- the assertion is the field's declared type: vi.hoisted infers the record from this literal const { captured } = vi.hoisted(() => ({ captured: {} as { markRead?: MarkRead } })) vi.mock("../../src/ws/gateway.js", async (importOriginal) => { diff --git a/services/api/test/integration/cleanups-chat-pg.test.ts b/services/api/test/integration/cleanups-chat-pg.test.ts index 17774d53..bda0ab74 100644 --- a/services/api/test/integration/cleanups-chat-pg.test.ts +++ b/services/api/test/integration/cleanups-chat-pg.test.ts @@ -260,7 +260,7 @@ describe.skipIf(!pg)("cleanups + chat (integration)", () => { const authServices = makeAuthServices({ stores, cache, - mailer: container.mailer as never, + mailer: container.mailer, oauthConfig: {}, verifier: new StubJwksVerifier(), now: () => Date.now(), @@ -331,7 +331,7 @@ describe.skipIf(!pg)("cleanups + chat (integration)", () => { const authServices = makeAuthServices({ stores, cache, - mailer: container.mailer as never, + mailer: container.mailer, oauthConfig: {}, verifier: new StubJwksVerifier(), now: () => Date.now(), diff --git a/services/api/test/integration/host-page-media-pg.test.ts b/services/api/test/integration/host-page-media-pg.test.ts index 53222f12..45cd55fc 100644 --- a/services/api/test/integration/host-page-media-pg.test.ts +++ b/services/api/test/integration/host-page-media-pg.test.ts @@ -60,7 +60,7 @@ describe.skipIf(!pg)("event page media binding (integration)", () => { id: "b1", kind: "sponsors", entries: [{ name: "Acme", logoMediaId: mediaId, url: "https://acme.example" }], - } as EventPageBlock + } } it("claims a fresh upload used as a sponsor logo and records the binding", async () => { diff --git a/services/api/test/integration/moderation-destination-refs-pg.test.ts b/services/api/test/integration/moderation-destination-refs-pg.test.ts index 8a98ae4f..d2868e46 100644 --- a/services/api/test/integration/moderation-destination-refs-pg.test.ts +++ b/services/api/test/integration/moderation-destination-refs-pg.test.ts @@ -8,7 +8,6 @@ import { randomUUID } from "node:crypto" import postgres from "postgres" import { withPg, testHandle, type PgHarness } from "../helpers/pg.js" import { seedCleanup } from "../helpers/cleanups.js" -import type { Sql } from "../../src/db/client.js" import { makeDrizzleModerationRepository } from "../../src/services/admin/moderation-repository.drizzle.js" import type { ModerationRepository } from "../../src/services/admin/moderation-repository.js" @@ -208,7 +207,7 @@ describe.skipIf(!pg)("F160: batched destination refs match the per-item lookup", debug: (_conn: number, query: string) => { statements.push(query) }, - }) as Sql + }) try { const spied = makeDrizzleModerationRepository(spy) const { records } = await spied.listOpen({ q: null, filter: "all", cursor: null, limit: 25 }) @@ -281,7 +280,7 @@ describe.skipIf(!pg)("F160: batched destination refs match the per-item lookup", debug: (_conn: number, query: string) => { statements.push(query) }, - }) as Sql + }) try { const spied = makeDrizzleModerationRepository(spy) const { records } = await spied.listOpen({ q: null, filter: "all", cursor: null, limit: 25 }) diff --git a/services/api/test/integration/perf-indexes-pg.test.ts b/services/api/test/integration/perf-indexes-pg.test.ts index d73a4c31..a8639c78 100644 --- a/services/api/test/integration/perf-indexes-pg.test.ts +++ b/services/api/test/integration/perf-indexes-pg.test.ts @@ -69,7 +69,7 @@ describe.skipIf(!pg)("performance indexes 0186-0192 (integration)", () => { debug: (_conn: number, query: string, params: unknown[]) => { if (query.includes("JOIN follows_people f")) captured = { query, params } }, - }) as unknown as Sql + }) }) afterAll(async () => { diff --git a/services/api/test/integration/signup-seats-pg.test.ts b/services/api/test/integration/signup-seats-pg.test.ts index 8202d7d1..a7573a23 100644 --- a/services/api/test/integration/signup-seats-pg.test.ts +++ b/services/api/test/integration/signup-seats-pg.test.ts @@ -203,7 +203,7 @@ describe.skipIf(!pg)("signup seats (integration)", () => { expect(await seatRows(cleanupId, volunteer)).toEqual([ { - seat_id: expect.any(String) as unknown as string, + seat_id: expect.any(String), seat_status: "cancelled", registration_status: "cancelled", }, diff --git a/services/api/test/integration/threads-lateral-bound-pg.test.ts b/services/api/test/integration/threads-lateral-bound-pg.test.ts index ceaf1657..115f9a1c 100644 --- a/services/api/test/integration/threads-lateral-bound-pg.test.ts +++ b/services/api/test/integration/threads-lateral-bound-pg.test.ts @@ -44,7 +44,7 @@ describe.skipIf(!pg)("threads inbox last-message LATERAL (integration)", () => { debug: (_conn: number, query: string, params: unknown[]) => { if (query.includes("report_chat_members")) captured = { query, params } }, - }) as unknown as Sql + }) }) afterAll(async () => { diff --git a/services/api/test/unit/abuse-checks-adapter.test.ts b/services/api/test/unit/abuse-checks-adapter.test.ts index ac2d609a..a9b96152 100644 --- a/services/api/test/unit/abuse-checks-adapter.test.ts +++ b/services/api/test/unit/abuse-checks-adapter.test.ts @@ -99,7 +99,7 @@ describe("RealAbuseChecks.verifyTurnstile hostname/action binding (L16)", () => ok: true, status: 200, json: () => Promise.resolve(body), - }) as unknown as typeof fetch + }) } afterEach(() => { diff --git a/services/api/test/unit/adapters-http-fetch.test.ts b/services/api/test/unit/adapters-http-fetch.test.ts index 41935b88..ceda9c29 100644 --- a/services/api/test/unit/adapters-http-fetch.test.ts +++ b/services/api/test/unit/adapters-http-fetch.test.ts @@ -201,14 +201,14 @@ describe("fetchJsonWithTimeout", () => { const single = streamResponse(JSON.stringify({ a: 1 })) const r1 = await fetchJsonWithTimeout<{ a: number }>("https://x/y", { timeoutMs: 50, - fetchImpl: (async () => single.res) as unknown as typeof fetch, + fetchImpl: async () => single.res, }) expect(r1).toEqual({ ok: true, status: 200, json: { a: 1 } }) const multi = streamResponse(JSON.stringify({ hello: "world", n: 42 }), { chunks: 5 }) const r2 = await fetchJsonWithTimeout<{ hello: string; n: number }>("https://x/y", { timeoutMs: 50, - fetchImpl: (async () => multi.res) as unknown as typeof fetch, + fetchImpl: async () => multi.res, }) expect(r2.ok && r2.json).toEqual({ hello: "world", n: 42 }) }) @@ -218,7 +218,7 @@ describe("fetchJsonWithTimeout", () => { const result = await fetchJsonWithTimeout("https://x/y", { timeoutMs: 50, maxBytes: 100, - fetchImpl: (async () => big.res) as unknown as typeof fetch, + fetchImpl: async () => big.res, }) expect(result.ok).toBe(false) expect(result.ok === false && result.kind).toBe("body") @@ -234,7 +234,7 @@ describe("fetchJsonWithTimeout", () => { const result = await fetchJsonWithTimeout("https://x/y", { timeoutMs: 50, maxBytes: 500, - fetchImpl: (async () => lying.res) as unknown as typeof fetch, + fetchImpl: async () => lying.res, }) expect(result.ok === false && result.kind).toBe("body") expect(result.ok === false && result.kind === "body" && result.error).toBeInstanceOf( @@ -255,9 +255,7 @@ describe("fetchJsonWithTimeout", () => { it("resolves globalThis.fetch at CALL time when no fetchImpl is injected", async () => { const original = globalThis.fetch try { - globalThis.fetch = vi.fn(async () => - jsonResponse({ via: "global" }), - ) as unknown as typeof fetch + globalThis.fetch = vi.fn(async () => jsonResponse({ via: "global" })) const result = await fetchJsonWithTimeout<{ via: string }>("https://x/y", { timeoutMs: 50 }) expect(result.ok && result.json).toEqual({ via: "global" }) } finally { diff --git a/services/api/test/unit/adapters-i18n-messages.test.ts b/services/api/test/unit/adapters-i18n-messages.test.ts index dc4a306d..0ffffaf5 100644 --- a/services/api/test/unit/adapters-i18n-messages.test.ts +++ b/services/api/test/unit/adapters-i18n-messages.test.ts @@ -52,7 +52,7 @@ describe("renderMessage", () => { it("names an event-team role in the reader's own language, never as the raw enum", () => { for (const locale of ["en", "es", "de", "ko"] as const) { for (const role of ["cohost", "coordinator", "staff"] as const) { - const label = renderMessage(locale, `role.${role}` as MessageKey) + const label = renderMessage(locale, `role.${role}`) expect(label).not.toBe(`role.${role}`) const body = renderMessage(locale, "notification.event_team_invite.body", { title: "Beach cleanup", diff --git a/services/api/test/unit/adapters-mailer-oci.test.ts b/services/api/test/unit/adapters-mailer-oci.test.ts index 578cc27d..c130d550 100644 --- a/services/api/test/unit/adapters-mailer-oci.test.ts +++ b/services/api/test/unit/adapters-mailer-oci.test.ts @@ -44,7 +44,7 @@ function outbound(overrides: Partial = {}): OutboundEmail { subject: "Case ABC123", text: "A pothole was reported.", ...overrides, - } as OutboundEmail + } } beforeEach(() => { diff --git a/services/api/test/unit/admin-host-platform-routes.test.ts b/services/api/test/unit/admin-host-platform-routes.test.ts index b61ac411..afb7400e 100644 --- a/services/api/test/unit/admin-host-platform-routes.test.ts +++ b/services/api/test/unit/admin-host-platform-routes.test.ts @@ -198,7 +198,7 @@ async function createOrg( }, }) if (res.statusCode !== 201) throw new Error(`create failed: ${res.statusCode} ${res.body}`) - return res.json() as { id: string; slug: string } + return res.json<{ id: string; slug: string }>() } async function seedPendingOrg(orgs: InMemoryOrganizationRepository): Promise { @@ -237,7 +237,7 @@ describe("admin signup-page moderation", () => { it("lists pages with the operator moderation state", async () => { const res = await h.app.inject({ method: "GET", url: "/v1/admin/pages" }) expect(res.statusCode).toBe(200) - const body = res.json() as { items: { cleanupId: string; viewCount: number }[] } + const body = res.json<{ items: { cleanupId: string; viewCount: number }[] }>() expect(body.items).toHaveLength(1) expect(body.items[0]?.cleanupId).toBe(CLEANUP) expect(body.items[0]?.viewCount).toBe(12) @@ -250,7 +250,7 @@ describe("admin signup-page moderation", () => { payload: { flagged: true, reason: "impersonates a city agency" }, }) expect(res.statusCode).toBe(200) - expect((res.json() as { flaggedAt: string | null }).flaggedAt).not.toBeNull() + expect(res.json<{ flaggedAt: string | null }>().flaggedAt).not.toBeNull() expect(h.pages[0]?.flagReason).toBe("impersonates a city agency") }) @@ -261,7 +261,7 @@ describe("admin signup-page moderation", () => { payload: { reason: "off-platform payment link" }, }) expect(res.statusCode).toBe(200) - expect((res.json() as { status: string }).status).toBe("unpublished") + expect(res.json<{ status: string }>().status).toBe("unpublished") }) it("404s a page that does not exist rather than inventing one", async () => { @@ -288,7 +288,7 @@ describe("admin media read", () => { const h = await harness() const res = await h.app.inject({ method: "GET", url: `/v1/admin/media/${MEDIA}` }) expect(res.statusCode).toBe(200) - const body = res.json() as { media: { id: string; url: string }; expiresAt: string } + const body = res.json<{ media: { id: string; url: string }; expiresAt: string }>() expect(body.media.id).toBe(MEDIA) expect(body.media.url.length).toBeGreaterThan(0) expect(Date.parse(body.expiresAt)).toBeGreaterThan(Date.now()) @@ -314,7 +314,7 @@ describe("admin legal versions", () => { const h = await harness() const res = await h.app.inject({ method: "GET", url: "/v1/admin/legal/versions" }) expect(res.statusCode).toBe(200) - const body = res.json() as { documents: { type: string; version: string; sha256: string }[] } + const body = res.json<{ documents: { type: string; version: string; sha256: string }[] }>() expect(body.documents.length).toBeGreaterThan(0) for (const doc of body.documents) { expect(doc.version.length).toBeGreaterThan(0) @@ -400,14 +400,14 @@ describe("admin host list", () => { const res = await h.app.inject({ method: "GET", url: "/v1/admin/hosts" }) expect(res.statusCode).toBe(200) - const body = res.json() as { + const body = res.json<{ items: { host: { id: string } messagingSuspended: boolean broadcastCount: number windowDays: number }[] - } + }>() expect(body.items).toHaveLength(1) expect(body.items[0]?.host.id).toBe(HOST) expect(body.items[0]?.messagingSuspended).toBe(true) @@ -425,10 +425,10 @@ describe("admin host list", () => { }) const suspended = await h.app.inject({ method: "GET", url: "/v1/admin/hosts?suspended=true" }) - expect((suspended.json() as { items: unknown[] }).items).toHaveLength(1) + expect(suspended.json<{ items: unknown[] }>().items).toHaveLength(1) const active = await h.app.inject({ method: "GET", url: "/v1/admin/hosts?suspended=false" }) - expect((active.json() as { items: unknown[] }).items).toHaveLength(0) + expect(active.json<{ items: unknown[] }>().items).toHaveLength(0) }) it("rejects an activity window longer than a year", async () => { @@ -514,28 +514,28 @@ describe("admin org management (0.41.0)", () => { }) const all = await h.app.inject({ method: "GET", url: "/v1/admin/orgs" }) expect(all.statusCode).toBe(200) - const body = all.json() as { + const body = all.json<{ items: { slug: string }[] counts: unknown nextCursor: string | null - } + }>() expect(body.items.map((o) => o.slug).sort()).toEqual(["reach-out-la", "second"]) expect(body.counts).toEqual({ all: 2, verified: 1, pending: 0, suspended: 1 }) expect(h.audits.map((a) => a.action)).toContain("org.list_viewed") const suspended = await h.app.inject({ method: "GET", url: "/v1/admin/orgs?suspended=true" }) - expect((suspended.json() as { items: { slug: string }[] }).items.map((o) => o.slug)).toEqual([ + expect(suspended.json<{ items: { slug: string }[] }>().items.map((o) => o.slug)).toEqual([ "second", ]) const verified = await h.app.inject({ method: "GET", url: "/v1/admin/orgs?verified=verified&kind=nonprofit", }) - expect((verified.json() as { items: { slug: string }[] }).items.map((o) => o.slug)).toEqual([ + expect(verified.json<{ items: { slug: string }[] }>().items.map((o) => o.slug)).toEqual([ "reach-out-la", ]) const q = await h.app.inject({ method: "GET", url: "/v1/admin/orgs?q=reach" }) - expect((q.json() as { items: unknown[] }).items).toHaveLength(1) + expect(q.json<{ items: unknown[] }>().items).toHaveLength(1) const bad = await h.app.inject({ method: "GET", url: "/v1/admin/orgs?suspended=maybe" }) expect(bad.statusCode).toBe(422) }) @@ -627,7 +627,7 @@ describe("admin org management (0.41.0)", () => { const list = await h.app.inject({ method: "GET", url: `/v1/admin/orgs/${a.id}/members` }) expect(list.statusCode).toBe(200) - expect((list.json() as { items: { user: { handle: string }; role: string }[] }).items).toEqual([ + expect(list.json<{ items: { user: { handle: string }; role: string }[] }>().items).toEqual([ expect.objectContaining({ user: expect.objectContaining({ handle: "ada" }), role: "owner" }), expect.objectContaining({ user: expect.objectContaining({ handle: "bea" }), role: "admin" }), ]) @@ -688,7 +688,7 @@ describe("admin org management (0.41.0)", () => { const index = h.orgs.members.findIndex((m) => m.organizationId === a.id && m.role === "owner") h.orgs.members.splice(index, 1) const before = await h.app.inject({ method: "GET", url: `/v1/admin/orgs/${a.id}` }) - expect((before.json() as { owner: unknown }).owner).toBeNull() + expect(before.json<{ owner: unknown }>().owner).toBeNull() const add = await h.app.inject({ method: "POST", url: `/v1/admin/orgs/${a.id}/members`, @@ -704,7 +704,7 @@ describe("admin org management (0.41.0)", () => { reason: "repair", }) const after = await h.app.inject({ method: "GET", url: `/v1/admin/orgs/${a.id}` }) - expect((after.json() as { owner: { id: string } }).owner).toMatchObject({ id: SECOND }) + expect(after.json<{ owner: { id: string } }>().owner).toMatchObject({ id: SECOND }) expect(h.notes).toEqual([ { userId: HOST, type: "org_invite" }, { userId: SECOND, type: "org_invite" }, @@ -731,7 +731,7 @@ describe("admin org management (0.41.0)", () => { const all = await h.app.inject({ method: "GET", url: `/v1/admin/orgs/${a.id}/events` }) expect(all.statusCode).toBe(200) - const items = (all.json() as { items: { title: string; id: string }[] }).items + const items = all.json<{ items: { title: string; id: string }[] }>().items expect(items.map((e) => e.title)).toEqual(["Future sweep", "Past sweep"]) expect(items[0]?.id).toBe(CLEANUP) @@ -739,14 +739,14 @@ describe("admin org management (0.41.0)", () => { method: "GET", url: `/v1/admin/orgs/${a.id}/events?when=upcoming`, }) - expect((upcoming.json() as { items: { title: string }[] }).items.map((e) => e.title)).toEqual([ + expect(upcoming.json<{ items: { title: string }[] }>().items.map((e) => e.title)).toEqual([ "Future sweep", ]) const past = await h.app.inject({ method: "GET", url: `/v1/admin/orgs/${a.id}/events?when=past`, }) - expect((past.json() as { items: { title: string }[] }).items.map((e) => e.title)).toEqual([ + expect(past.json<{ items: { title: string }[] }>().items.map((e) => e.title)).toEqual([ "Past sweep", ]) expect(h.audits.map((x) => x.action)).toContain("org.events_viewed") diff --git a/services/api/test/unit/admin-inbox.test.ts b/services/api/test/unit/admin-inbox.test.ts index 58e8b555..c383e670 100644 --- a/services/api/test/unit/admin-inbox.test.ts +++ b/services/api/test/unit/admin-inbox.test.ts @@ -355,7 +355,7 @@ describe("POST /admin/inbox/:id/status", () => { }) expect(badStatus.statusCode).toBe(422) expect( - (badStatus.json() as { code: string; fields?: Record }).fields, + badStatus.json<{ code: string; fields?: Record }>().fields, ).toHaveProperty("status") expect(harness.repo.rows.find((r) => r.id === id)?.status).toBe("unread") diff --git a/services/api/test/unit/admin-report-chat.test.ts b/services/api/test/unit/admin-report-chat.test.ts index c7f10a18..040e25c9 100644 --- a/services/api/test/unit/admin-report-chat.test.ts +++ b/services/api/test/unit/admin-report-chat.test.ts @@ -36,7 +36,7 @@ function messageDTO(overrides: Partial = {}): ChatMessageDTO { reactions: [], mentions: [], ...overrides, - } as ChatMessageDTO + } } interface SendHarness { diff --git a/services/api/test/unit/auth-oauth-security.test.ts b/services/api/test/unit/auth-oauth-security.test.ts index 755df08b..cb2f5120 100644 --- a/services/api/test/unit/auth-oauth-security.test.ts +++ b/services/api/test/unit/auth-oauth-security.test.ts @@ -276,7 +276,7 @@ describe("POST /v1/auth/google with an unverified email", () => { }) expect(res.statusCode).toBe(200) - const body = res.json() as { user: { id: string; email: string | null }; token: string } + const body = res.json<{ user: { id: string; email: string | null }; token: string }>() expect(body.user.id).not.toBe(victim.userId) expect(body.user.email ?? null).toBeNull() const session = await harness.app.inject({ diff --git a/services/api/test/unit/auth-route-mounting.test.ts b/services/api/test/unit/auth-route-mounting.test.ts index b31646c2..32d3d9c5 100644 --- a/services/api/test/unit/auth-route-mounting.test.ts +++ b/services/api/test/unit/auth-route-mounting.test.ts @@ -33,7 +33,7 @@ describe("auth/admin route mounting is gated on the auth bundle", () => { // The exact failing request: route-missing, not a domain 404. const admin = await app.inject({ method: "GET", url: "/v1/admin/auth/session" }) expect(admin.statusCode).toBe(404) - const adminBody = admin.json() as { code?: string; message?: string } + const adminBody = admin.json<{ code?: string; message?: string }>() expect(adminBody.code).toBe("NOT_FOUND") expect(adminBody.message?.startsWith(routeMissingPrefix("GET"))).toBe(true) @@ -42,7 +42,7 @@ describe("auth/admin route mounting is gated on the auth bundle", () => { const citizen = await app.inject({ method: "GET", url: "/v1/auth/session" }) expect(citizen.statusCode).toBe(404) expect( - (citizen.json() as { message?: string }).message?.startsWith(routeMissingPrefix("GET")), + citizen.json<{ message?: string }>().message?.startsWith(routeMissingPrefix("GET")), ).toBe(true) const health = await app.inject({ method: "GET", url: "/healthz" }) diff --git a/services/api/test/unit/auth-routes.test.ts b/services/api/test/unit/auth-routes.test.ts index 92aaf572..997490e2 100644 --- a/services/api/test/unit/auth-routes.test.ts +++ b/services/api/test/unit/auth-routes.test.ts @@ -766,10 +766,10 @@ describe("auth routes: first-run registration (handle availability + PUT /me/pro headers: { "x-client": "mobile" }, payload: { email, code }, }) - return res.json() as { + return res.json<{ token: string user: { profileComplete: boolean; handle: string | null } - } + }>() } it("a fresh account is profileComplete:false with a generated placeholder handle (the gate trigger)", async () => { diff --git a/services/api/test/unit/certificate-routes.test.ts b/services/api/test/unit/certificate-routes.test.ts index 6f7b4c19..006d795c 100644 --- a/services/api/test/unit/certificate-routes.test.ts +++ b/services/api/test/unit/certificate-routes.test.ts @@ -224,10 +224,10 @@ describe("POST /me/volunteer-hours/certificates", () => { const res = await issue(h.app, me) expect(res.statusCode).toBe(200) - const body = res.json() as { + const body = res.json<{ certificate: { code: string; url: string | null; status: string; totalHours: number } reused?: boolean - } + }>() expect(body.certificate.code).toMatch(CERTIFICATE_CODE_RE) expect(body.certificate.status).toBe("valid") expect(body.certificate.totalHours).toBe(4.5) @@ -342,7 +342,7 @@ describe("GET /me/volunteer-hours/certificates", () => { headers: bearer(me), }) expect(res.statusCode).toBe(200) - const list = res.json() as { certificates: { code: string; url: string | null }[] } + const list = res.json<{ certificates: { code: string; url: string | null }[] }>() expect(list.certificates).toHaveLength(1) expect(list.certificates[0]!.url).toBeNull() expect(h.storage.presigns).toHaveLength(presignsAfterIssue) @@ -545,7 +545,7 @@ describe("GET /service-hours/verify/:code (public)", () => { const res = await verifyCode(h, code) expect(res.statusCode).toBe(200) - const body = res.json() as Record + const body = res.json>() expect(body.status).toBe("valid") expect(body.holderName).toBe("Jane Doe") expect(body.totalHours).toBe(12.5) @@ -568,7 +568,7 @@ describe("GET /service-hours/verify/:code (public)", () => { const res = await verifyCode(h, code) expect(res.statusCode).toBe(200) - const body = res.json() as Record + const body = res.json>() expect(body.status).toBe("revoked") expect(body.revokedReason).toBe("holder") // The person holding the paper learns WHY it is not good. @@ -582,7 +582,7 @@ describe("GET /service-hours/verify/:code (public)", () => { const res = await verifyCode(h, code) expect(res.statusCode).toBe(200) - const body = res.json() as Record + const body = res.json>() expect(body.status).toBe("revoked") expect(body.revokedReason).toBe("account_closed") expect(Object.keys(body)).not.toContain("holderName") diff --git a/services/api/test/unit/certificate-verify-url.test.ts b/services/api/test/unit/certificate-verify-url.test.ts index 2dc8962c..7962dd5a 100644 --- a/services/api/test/unit/certificate-verify-url.test.ts +++ b/services/api/test/unit/certificate-verify-url.test.ts @@ -8,9 +8,9 @@ import type { VolunteerHoursEntryView } from "../../src/services/volunteer-hours const qrPayloads = vi.hoisted(() => [] as string[]) vi.mock("qrcode-generator", async (importOriginal) => { - const actual = (await importOriginal()) as { + const actual = await importOriginal<{ default: (t: number, e: string) => { addData(d: string): void } - } + }>() return { default: (typeNumber: number, level: string) => { const qr = actual.default(typeNumber, level) diff --git a/services/api/test/unit/chat-block-gates.test.ts b/services/api/test/unit/chat-block-gates.test.ts index 45f60119..7e465102 100644 --- a/services/api/test/unit/chat-block-gates.test.ts +++ b/services/api/test/unit/chat-block-gates.test.ts @@ -56,7 +56,7 @@ describe("M11: the report-room fan-out skips blocked pairs", () => { const notify = makeReportChatNotifier({ notificationService: { createNotificationsReportingFailures: spy.createNotificationsReportingFailures, - } as never, + }, reportChatRepo: { listMemberIds: () => Promise.resolve([ACTOR, BLOCKED, NEUTRAL]) }, isMuted: () => Promise.resolve(false), roomKeyFor: (_k, id) => `report:${id}`, @@ -138,7 +138,7 @@ describe("M11 batch seam: blockedIdsFor replaces the per-candidate gate, with th const notify = makeReportChatNotifier({ notificationService: { createNotificationsReportingFailures: spy.createNotificationsReportingFailures, - } as never, + }, reportChatRepo: { listMemberIds: () => Promise.resolve([ACTOR, BLOCKED, NEUTRAL]) }, isMuted: () => Promise.resolve(false), roomKeyFor: (_k, id) => `report:${id}`, @@ -175,7 +175,7 @@ describe("M11: the group-room fan-out skips blocked pairs", () => { const notify = makeGroupChatNotifier({ notificationService: { createNotificationsReportingFailures: spy.createNotificationsReportingFailures, - } as never, + }, groupRepo: { listMemberIds: () => Promise.resolve([ACTOR, BLOCKED, NEUTRAL]) }, isMuted: () => Promise.resolve(false), roomKeyFor: (_k, id) => `group:${id}`, @@ -197,9 +197,7 @@ describe("L10: the dm lane's pin power respects blocks", () => { }) it("a blocked participant holds NO powers in the thread", async () => { - const resolve = makeChatPowersResolver( - deps({ isDmBlocked: () => Promise.resolve(true) }) as never, - ) + const resolve = makeChatPowersResolver(deps({ isDmBlocked: () => Promise.resolve(true) })) expect(await resolve({ roomKind: "dm", roomId: ROOM, userId: ACTOR })).toEqual({ canPin: false, canDeleteOthers: false, @@ -208,9 +206,7 @@ describe("L10: the dm lane's pin power respects blocks", () => { }) it("an unblocked participant keeps canPin (and never canDeleteOthers)", async () => { - const resolve = makeChatPowersResolver( - deps({ isDmBlocked: () => Promise.resolve(false) }) as never, - ) + const resolve = makeChatPowersResolver(deps({ isDmBlocked: () => Promise.resolve(false) })) expect(await resolve({ roomKind: "dm", roomId: ROOM, userId: ACTOR })).toEqual({ canPin: true, canDeleteOthers: false, @@ -221,7 +217,7 @@ describe("L10: the dm lane's pin power respects blocks", () => { it("a non-participant is refused before the block lookup is even consulted", async () => { const isDmBlocked = vi.fn(() => Promise.resolve(false)) const resolve = makeChatPowersResolver( - deps({ isDmParticipant: () => Promise.resolve(false), isDmBlocked }) as never, + deps({ isDmParticipant: () => Promise.resolve(false), isDmBlocked }), ) expect(await resolve({ roomKind: "dm", roomId: ROOM, userId: ACTOR })).toMatchObject({ canPin: false, diff --git a/services/api/test/unit/chat-broadcast-viewer-fields.test.ts b/services/api/test/unit/chat-broadcast-viewer-fields.test.ts index b5ac295d..a63815f0 100644 --- a/services/api/test/unit/chat-broadcast-viewer-fields.test.ts +++ b/services/api/test/unit/chat-broadcast-viewer-fields.test.ts @@ -9,10 +9,7 @@ import { InMemoryDmRepository, } from "../../src/services/dm-repository.memory.js" import { deleteMessageWithPowers } from "../../src/routes/chat-route-helpers.js" -import { - makeChatEditService, - type ChatEditServiceDeps, -} from "../../src/services/chat-edit-service.js" +import { makeChatEditService } from "../../src/services/chat-edit-service.js" import { makeChatPollService, type ChatPollServiceDeps, @@ -224,7 +221,7 @@ describe("F043 edits fan out without the editor's viewer fields", () => { MESSAGE, ) const edits = makeChatEditService({ - chat: repo as unknown as ChatEditServiceDeps["chat"], + chat: repo, isCleanupMember: () => Promise.resolve(true), broadcastEvent: (roomKey, frame) => { events.push({ roomKey, frame }) diff --git a/services/api/test/unit/chat-mention-resolver.test.ts b/services/api/test/unit/chat-mention-resolver.test.ts index fc885a9b..b7764077 100644 --- a/services/api/test/unit/chat-mention-resolver.test.ts +++ b/services/api/test/unit/chat-mention-resolver.test.ts @@ -55,7 +55,7 @@ function resolverOver( listReportChatMemberIds: kind === "report" ? rosterLookup(roster, REPORT_CHAT_MEMBER_SCAN_CAP, seen) : none, listGroupMemberIds: kind === "group" ? rosterLookup(roster, GROUP_MEMBER_SCAN_CAP, seen) : none, - } as ChatMentionResolverDeps + } } describe("mention scope checks membership of the mentioned users, not a capped roster", () => { diff --git a/services/api/test/unit/chat-poll-notifier.test.ts b/services/api/test/unit/chat-poll-notifier.test.ts index e5d4f85b..36a38794 100644 --- a/services/api/test/unit/chat-poll-notifier.test.ts +++ b/services/api/test/unit/chat-poll-notifier.test.ts @@ -30,6 +30,7 @@ import type { Container } from "../../src/di.js" */ const stub = vi.hoisted(() => ({ /** The InMemoryNotificationRepository instance, installed per test (see beforeEach). */ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- the assertion is the field's declared type: vi.hoisted infers the record from this literal notifRepo: undefined as unknown, reportMembers: [] as string[], groupMembers: [] as string[], @@ -158,7 +159,7 @@ function pollMessage( { idx: 1, text: "Sun", count: 0, mine: false }, ], }, - } as ChatMessageDTO + } } /** diff --git a/services/api/test/unit/chat-pubsub-redis.test.ts b/services/api/test/unit/chat-pubsub-redis.test.ts index 4cd831ac..ebb147c9 100644 --- a/services/api/test/unit/chat-pubsub-redis.test.ts +++ b/services/api/test/unit/chat-pubsub-redis.test.ts @@ -20,7 +20,7 @@ const BOB = "22222222-2222-2222-2222-222222222222" // ioredis-mock is structurally an ioredis client; cast through unknown at this adapter-test boundary. function makeMockRedis(): RedisClient { - return new RedisMock() as unknown as RedisClient + return new RedisMock() } const created: Array<{ close: () => Promise; redis: RedisClient }> = [] @@ -63,13 +63,13 @@ describe("RedisChatPubSub over ioredis-mock", () => { // the root for each worker's pub/sub to emulate two nodes on one Redis. const root = makeMockRedis() - const redis1 = root.duplicate() as unknown as RedisClient + const redis1 = root.duplicate() const pubsub1 = new RedisChatPubSub(redis1) const repo1 = new InMemoryChatRepository() repo1.registerSender({ id: ALICE, displayName: "Alice" }) const worker1 = new WsChatService({ repo: repo1, pubsub: pubsub1 }) - const redis2 = root.duplicate() as unknown as RedisClient + const redis2 = root.duplicate() const pubsub2 = new RedisChatPubSub(redis2) const repo2 = new InMemoryChatRepository() const worker2 = new WsChatService({ repo: repo2, pubsub: pubsub2 }) diff --git a/services/api/test/unit/chat-room-notifier-wiring.test.ts b/services/api/test/unit/chat-room-notifier-wiring.test.ts index a3b82976..06bb9fd6 100644 --- a/services/api/test/unit/chat-room-notifier-wiring.test.ts +++ b/services/api/test/unit/chat-room-notifier-wiring.test.ts @@ -43,7 +43,7 @@ function harness(opts: { useFakeChat?: boolean; usesRealRedis?: boolean } = {}): get redisOpened() { return state.redisOpened }, - } as Harness + } } describe("makeContainerRoomFanoutDeps", () => { @@ -189,10 +189,13 @@ describe("makeWindowClaim", () => { const { claim, counters } = claimHarness() vi.spyOn(counters, "incr").mockRejectedValue(new Error("redis down")) const warn = vi.fn() - const claimWithLog = makeWindowClaim({ getCounterStore: () => counters }, { - warn, - error: vi.fn(), - } as never) + const claimWithLog = makeWindowClaim( + { getCounterStore: () => counters }, + { + warn, + error: vi.fn(), + }, + ) expect(await claim("report", "r1", 15_000)).toBe(true) expect(await claimWithLog("report", "r1", 15_000)).toBe(true) diff --git a/services/api/test/unit/city-forward-logger-wiring.test.ts b/services/api/test/unit/city-forward-logger-wiring.test.ts index 35f96538..8c271944 100644 --- a/services/api/test/unit/city-forward-logger-wiring.test.ts +++ b/services/api/test/unit/city-forward-logger-wiring.test.ts @@ -18,6 +18,7 @@ import { } from "../../src/services/dm-repository.memory.js" const { captured, mail } = vi.hoisted(() => ({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- the assertion is the field's declared type: vi.hoisted infers the record from this literal captured: {} as { onReportMessage?: OnReportMessage }, mail: { sendFails: false }, })) @@ -103,15 +104,15 @@ function reportChatStub(): ReportChatRepository { } return { isMember: () => Promise.resolve(true), - roleOf: notImpl("roleOf") as never, + roleOf: notImpl("roleOf"), advanceReadWatermark: () => Promise.resolve(), - markRead: notImpl("markRead") as never, - join: notImpl("join") as never, - leave: notImpl("leave") as never, - insertSystemMessage: notImpl("insertSystemMessage") as never, + markRead: notImpl("markRead"), + join: notImpl("join"), + leave: notImpl("leave"), + insertSystemMessage: notImpl("insertSystemMessage"), listMemberIds: () => Promise.resolve([]), - countMembers: notImpl("countMembers") as never, - listMembers: notImpl("listMembers") as never, + countMembers: notImpl("countMembers"), + listMembers: notImpl("listMembers"), } } diff --git a/services/api/test/unit/city-forward-logging.test.ts b/services/api/test/unit/city-forward-logging.test.ts index 519ea3d7..e1dc1db3 100644 --- a/services/api/test/unit/city-forward-logging.test.ts +++ b/services/api/test/unit/city-forward-logging.test.ts @@ -76,7 +76,7 @@ describe("city forward failures are logged, not swallowed", () => { const audit: ReportForwardAuditRepository = { recordMention: () => Promise.reject(new Error("audit down")), markForwarded: () => Promise.resolve(), - } as unknown as ReportForwardAuditRepository + } const res = await forwardReportCityMention(mail({}), ctx, "@sf please fix", CREATED, { logger, audit, diff --git a/services/api/test/unit/city-forward-wiring.test.ts b/services/api/test/unit/city-forward-wiring.test.ts index 7d9f40c9..04657f50 100644 --- a/services/api/test/unit/city-forward-wiring.test.ts +++ b/services/api/test/unit/city-forward-wiring.test.ts @@ -26,6 +26,7 @@ import { } from "../../src/services/report-city-forward.js" const { captured, sent } = vi.hoisted(() => ({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- the assertion is the field's declared type: vi.hoisted infers the record from this literal captured: {} as { onReportMessage?: OnReportMessage }, sent: [] as { threadId: string; input: AppendOutboundInput }[], })) @@ -113,15 +114,15 @@ function reportChatStub(): ReportChatRepository { } return { isMember: () => Promise.resolve(true), - roleOf: notImpl("roleOf") as never, + roleOf: notImpl("roleOf"), advanceReadWatermark: () => Promise.resolve(), - markRead: notImpl("markRead") as never, - join: notImpl("join") as never, - leave: notImpl("leave") as never, - insertSystemMessage: notImpl("insertSystemMessage") as never, + markRead: notImpl("markRead"), + join: notImpl("join"), + leave: notImpl("leave"), + insertSystemMessage: notImpl("insertSystemMessage"), listMemberIds: () => Promise.resolve([]), - countMembers: notImpl("countMembers") as never, - listMembers: notImpl("listMembers") as never, + countMembers: notImpl("countMembers"), + listMembers: notImpl("listMembers"), } } diff --git a/services/api/test/unit/cleanup-service-security.test.ts b/services/api/test/unit/cleanup-service-security.test.ts index b5afb599..e01a443d 100644 --- a/services/api/test/unit/cleanup-service-security.test.ts +++ b/services/api/test/unit/cleanup-service-security.test.ts @@ -7,7 +7,6 @@ import { makeCleanupService, RESOURCE_REQUEST_PER_HOST_PER_DAY, RESOURCE_REQUEST_PER_JURISDICTION_PER_HOUR, - type CleanupService, } from "../../src/services/cleanup-service.js" const GEOID = "0644000" @@ -72,7 +71,7 @@ function harness() { return dto.id } - return { svc: svc as CleanupService, sent, eventHostedBy } + return { svc: svc, sent, eventHostedBy } } describe("resource request budgets", () => { diff --git a/services/api/test/unit/cleanup-slots-routes.test.ts b/services/api/test/unit/cleanup-slots-routes.test.ts index 897e3c31..abb1c196 100644 --- a/services/api/test/unit/cleanup-slots-routes.test.ts +++ b/services/api/test/unit/cleanup-slots-routes.test.ts @@ -258,7 +258,7 @@ describe("route configuration", () => { payload: { slotId: null }, }) expect(wrongVerb.statusCode).toBe(404) - expect((wrongVerb.json() as { message?: string }).message?.startsWith("Route POST ")).toBe(true) + expect(wrongVerb.json<{ message?: string }>().message?.startsWith("Route POST ")).toBe(true) }) it("declares the rate limit and CSRF in the source (the two configs a copy-paste route loses)", async () => { @@ -400,7 +400,7 @@ describe("slot windows over the wire", () => { }, }) expect(res.statusCode).toBe(422) - const body = res.json() as { code: string; fields: Record } + const body = res.json<{ code: string; fields: Record }>() expect(body.code).toBe("VALIDATION") expect(body.fields["slots.0.endsAt"]).toBe("set both a start and an end, or neither") }) @@ -428,7 +428,7 @@ describe("slot windows over the wire", () => { }, }) expect(res.statusCode).toBe(422) - expect((res.json() as { fields: Record }).fields["slots.0.endsAt"]).toBe( + expect(res.json<{ fields: Record }>().fields["slots.0.endsAt"]).toBe( "must be at least 15 minutes after startsAt", ) }) diff --git a/services/api/test/unit/cleanup-slots-sql.test.ts b/services/api/test/unit/cleanup-slots-sql.test.ts index 3b674678..59f0c7fb 100644 --- a/services/api/test/unit/cleanup-slots-sql.test.ts +++ b/services/api/test/unit/cleanup-slots-sql.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest" -import type { Sql } from "../../src/db/client.js" import { makeDrizzleCleanupRepository } from "../../src/services/cleanup-repository.drizzle.js" import type { CreateCleanupTxArgs, DesiredSlot } from "../../src/services/cleanup-repository.js" import { makeSqlRecorder, type SqlRecorder } from "../helpers/sql-recorder.js" @@ -37,7 +36,7 @@ async function createWith(slots: DesiredSlot[]): Promise { const rec = makeSqlRecorder() rec.on(/reference_counters/, [{ next_val: 1 }]) await expect( - makeDrizzleCleanupRepository(rec.sql as unknown as Sql).createCleanupTx(createArgs(slots)), + makeDrizzleCleanupRepository(rec.sql).createCleanupTx(createArgs(slots)), ).rejects.toMatchObject({ httpStatus: 500 }) return rec } diff --git a/services/api/test/unit/cleanup-status-derivation.test.ts b/services/api/test/unit/cleanup-status-derivation.test.ts index 0d12dd73..bc93b67c 100644 --- a/services/api/test/unit/cleanup-status-derivation.test.ts +++ b/services/api/test/unit/cleanup-status-derivation.test.ts @@ -96,6 +96,7 @@ describe("buildWhenFilter", () => { describe("deriveCleanupStatus (the TS twin the memory repositories use)", () => { const window = (startOffsetH: number, endOffsetH: number, cancelled = false) => ({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- keeps the literal union; the object property would otherwise widen to string status: (cancelled ? "cancelled" : "upcoming") as "cancelled" | "upcoming", scheduledAt: new Date(NOW + startOffsetH * HOUR).toISOString(), endsAt: new Date(NOW + endOffsetH * HOUR).toISOString(), diff --git a/services/api/test/unit/cleanups-routes.test.ts b/services/api/test/unit/cleanups-routes.test.ts index ac81982c..8ec0bf32 100644 --- a/services/api/test/unit/cleanups-routes.test.ts +++ b/services/api/test/unit/cleanups-routes.test.ts @@ -1252,7 +1252,7 @@ describe("GET /cleanups/:id/ics", () => { const res = await app.inject({ method: "GET", url: `/v1/cleanups/${id}/ics` }) expect(res.statusCode).toBe(200) - const body = res.json() as { ics: string; filename: string } + const body = res.json<{ ics: string; filename: string }>() expect(body.filename).toBe(`civfix-event-${id}.ics`) expect(body.ics.startsWith("BEGIN:VCALENDAR")).toBe(true) expect(body.ics).toContain("BEGIN:VEVENT") @@ -1268,7 +1268,7 @@ describe("GET /cleanups/:id/ics", () => { const res = await app.inject({ method: "GET", url: `/v1/cleanups/${id}/ics` }) expect(res.statusCode).toBe(200) - expect((res.json() as { ics: string }).ics).not.toContain("https://civfix.org/events/") + expect(res.json<{ ics: string }>().ics).not.toContain("https://civfix.org/events/") }) it("marks a cancelled event CANCELLED so a calendar client withdraws it", async () => { @@ -1283,7 +1283,7 @@ describe("GET /cleanups/:id/ics", () => { const res = await app.inject({ method: "GET", url: `/v1/cleanups/${id}/ics` }) expect(res.statusCode).toBe(200) - expect((res.json() as { ics: string }).ics).toContain("STATUS:CANCELLED") + expect(res.json<{ ics: string }>().ics).toContain("STATUS:CANCELLED") }) it("404s an event that does not exist rather than emitting an empty calendar", async () => { @@ -1341,7 +1341,7 @@ describe("GET /cleanups/:id/ics", () => { headers: auth(h.token), }) expect(res.statusCode).toBe(200) - const ics = (res.json() as { ics: string }).ics + const ics = res.json<{ ics: string }>().ics expect(ics).toContain(`DTSTART:${stamp(slotStart)}`) expect(ics).toContain(`DTEND:${stamp(slotEnd)}`) expect(ics).toContain("SUMMARY:Sweep (Morning sweep)") @@ -1353,7 +1353,7 @@ describe("GET /cleanups/:id/ics", () => { const res = await h.app.inject({ method: "GET", url: `/v1/cleanups/${id}/ics` }) expect(res.statusCode).toBe(200) - const ics = (res.json() as { ics: string }).ics + const ics = res.json<{ ics: string }>().ics expect(ics).toContain(`DTSTART:${stamp(FUTURE)}`) expect(ics).toContain(`DTEND:${stamp(eventEnd)}`) expect(ics).toContain("SUMMARY:Sweep") @@ -1388,7 +1388,7 @@ describe("GET /cleanups/:id/ics", () => { url: `/v1/cleanups/${id}/ics`, headers: auth(h.token), }) - const ics = (res.json() as { ics: string }).ics + const ics = res.json<{ ics: string }>().ics expect(ics).toContain(`DTSTART:${stamp(FUTURE)}`) expect(ics).toContain("SUMMARY:Sweep") expect(ics).not.toContain("Grill") diff --git a/services/api/test/unit/conversations-routes-security.test.ts b/services/api/test/unit/conversations-routes-security.test.ts index 7c732be5..675ed529 100644 --- a/services/api/test/unit/conversations-routes-security.test.ts +++ b/services/api/test/unit/conversations-routes-security.test.ts @@ -66,7 +66,7 @@ async function harnessWithoutGate(): Promise { headers: { "x-client": "mobile" }, payload: { email, code: mailer.lastOtpFor(email)! }, }) - const token = (verify.json() as { token: string }).token + const token = verify.json<{ token: string }>().token return { app, token, setMuted, setHidden, markRoomRead } } diff --git a/services/api/test/unit/counter-store-release.test.ts b/services/api/test/unit/counter-store-release.test.ts index bda4bab8..e710e5d6 100644 --- a/services/api/test/unit/counter-store-release.test.ts +++ b/services/api/test/unit/counter-store-release.test.ts @@ -11,7 +11,7 @@ const WINDOW_SECONDS = 60 const ELAPSED_WINDOW_MS = 3_000 function freshRedis(): RedisClient { - return new RedisMock() as unknown as RedisClient + return new RedisMock() } const stores: Array<[string, () => { store: CounterStore; redis?: RedisClient }]> = [ diff --git a/services/api/test/unit/feed-ranked-service.test.ts b/services/api/test/unit/feed-ranked-service.test.ts index 19c552b8..1b1ca0d3 100644 --- a/services/api/test/unit/feed-ranked-service.test.ts +++ b/services/api/test/unit/feed-ranked-service.test.ts @@ -116,7 +116,7 @@ function repoOver(over: Partial): PostRepository { listUserPosts: () => Promise.resolve({ items: [], nextCursor: null }), listSaves: () => Promise.resolve({ items: [], nextCursor: null }), ...over, - } as PostRepository + } } function recordingChannel(): { diff --git a/services/api/test/unit/feed-ranking.test.ts b/services/api/test/unit/feed-ranking.test.ts index c5b87d76..8005abd8 100644 --- a/services/api/test/unit/feed-ranking.test.ts +++ b/services/api/test/unit/feed-ranking.test.ts @@ -66,8 +66,8 @@ describe("feed ranking: each weight term in isolation", () => { for (const [flag, weight] of flags) { it(`adds exactly ${weight} when ${flag} is set`, () => { - const score = rawScore(candidate({ [flag]: true } as Partial), CFG) - expect(score - base).toBeCloseTo(CFG[weight] as number, 10) + const score = rawScore(candidate({ [flag]: true }), CFG) + expect(score - base).toBeCloseTo(CFG[weight], 10) }) } diff --git a/services/api/test/unit/glitchtip-scrub.test.ts b/services/api/test/unit/glitchtip-scrub.test.ts index 070505da..4af21296 100644 --- a/services/api/test/unit/glitchtip-scrub.test.ts +++ b/services/api/test/unit/glitchtip-scrub.test.ts @@ -95,9 +95,7 @@ describe("scrubEvent (exception + message redaction)", () => { const input = { exception: { values: [{ value: "x@y.com" }] } } const out = scrubEvent(input) expect((input.exception.values[0] as { value: string }).value).toBe("x@y.com") - expect((out.exception as { values: Array<{ value: string }> }).values[0]!.value).toBe( - "[redacted]", - ) + expect(out.exception.values[0]!.value).toBe("[redacted]") }) }) diff --git a/services/api/test/unit/guest-rsvp-notice-optout.test.ts b/services/api/test/unit/guest-rsvp-notice-optout.test.ts index d07cb21a..6dec890f 100644 --- a/services/api/test/unit/guest-rsvp-notice-optout.test.ts +++ b/services/api/test/unit/guest-rsvp-notice-optout.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest" -import type { GuestRsvpRequestRequest, GuestRsvpVerifyRequest } from "@civfix/shared" import { FakeAbuseChecks, FakeMailer, FakeSmsSender } from "@civfix/shared/fakes" import { InMemoryCacheClient } from "../../src/auth/cache.js" import { InMemoryCounterStore } from "../../src/abuse/counter-store.js" @@ -45,13 +44,10 @@ describe("guest sms notice: a failed opt-out write is not silent", () => { channel: "sms", phone: PHONE, turnstileToken: "ok", - } as GuestRsvpRequestRequest, - ctx, - ) - await service.verifyCode( - { id: EVENT_ID, channel: "sms", phone: PHONE, code: CODE } as GuestRsvpVerifyRequest, + }, ctx, ) + await service.verifyCode({ id: EVENT_ID, channel: "sms", phone: PHONE, code: CODE }, ctx) const guestId = repo.guests[0]?.id const writeFailed = new Error("db down") sms.send = () => Promise.reject(smsFailure("opted_out", "recipient opted out")) diff --git a/services/api/test/unit/guest-rsvp-refusal-subcodes.test.ts b/services/api/test/unit/guest-rsvp-refusal-subcodes.test.ts index 2c792d2b..218cf776 100644 --- a/services/api/test/unit/guest-rsvp-refusal-subcodes.test.ts +++ b/services/api/test/unit/guest-rsvp-refusal-subcodes.test.ts @@ -95,7 +95,7 @@ function request(extra: Partial = {}): GuestRsvpRequest email: "ada@example.org", turnstileToken: "ok", ...extra, - } as GuestRsvpRequestRequest + } } function verify(extra: Partial = {}): GuestRsvpVerifyRequest { @@ -105,7 +105,7 @@ function verify(extra: Partial = {}): GuestRsvpVerifyReq email: "ada@example.org", code: CODE, ...extra, - } as GuestRsvpVerifyRequest + } } function codesSent(mailer: FakeMailer): number { diff --git a/services/api/test/unit/guest-rsvp-refusal.test.ts b/services/api/test/unit/guest-rsvp-refusal.test.ts index dd39890d..872cb269 100644 --- a/services/api/test/unit/guest-rsvp-refusal.test.ts +++ b/services/api/test/unit/guest-rsvp-refusal.test.ts @@ -66,14 +66,14 @@ const request: GuestRsvpRequestRequest = { channel: "email", email: "ada@example.org", turnstileToken: "ok", -} as GuestRsvpRequestRequest +} const verify: GuestRsvpVerifyRequest = { id: EVENT_ID, channel: "email", email: "ada@example.org", code: CODE, -} as GuestRsvpVerifyRequest +} function confirmations(mailer: FakeMailer): number { return mailer.sent.filter((m) => m.template === "guest_confirmed").length @@ -169,7 +169,7 @@ describe("guest rsvp: the stored channel is the one the code was delivered on", channel: "sms", phone: "ada@example.org", code: CODE, - } as GuestRsvpVerifyRequest, + }, ctx, ) diff --git a/services/api/test/unit/guest-rsvp-security.test.ts b/services/api/test/unit/guest-rsvp-security.test.ts index 3d176eeb..9ae48c81 100644 --- a/services/api/test/unit/guest-rsvp-security.test.ts +++ b/services/api/test/unit/guest-rsvp-security.test.ts @@ -51,11 +51,11 @@ function emailRequest(id: string): GuestRsvpRequestRequest { channel: "email", email: "ada@example.org", turnstileToken: "ok", - } as GuestRsvpRequestRequest + } } function emailVerify(id: string): GuestRsvpVerifyRequest { - return { id, channel: "email", email: "ada@example.org", code: CODE } as GuestRsvpVerifyRequest + return { id, channel: "email", email: "ada@example.org", code: CODE } } async function rejectionOf(promise: Promise): Promise<{ code: string; message: string }> { diff --git a/services/api/test/unit/guest-rsvp-service.test.ts b/services/api/test/unit/guest-rsvp-service.test.ts index 2c56bcab..48abccff 100644 --- a/services/api/test/unit/guest-rsvp-service.test.ts +++ b/services/api/test/unit/guest-rsvp-service.test.ts @@ -127,7 +127,7 @@ function emailRequest(over: Partial = {}): GuestRsvpReq email: "ada@example.org", turnstileToken: "ok", ...over, - } as GuestRsvpRequestRequest + } } function smsRequest(over: Partial = {}): GuestRsvpRequestRequest { @@ -138,7 +138,7 @@ function smsRequest(over: Partial = {}): GuestRsvpReque phone: "+15552223333", turnstileToken: "ok", ...over, - } as GuestRsvpRequestRequest + } } function emailVerify(over: Partial = {}): GuestRsvpVerifyRequest { @@ -148,7 +148,7 @@ function emailVerify(over: Partial = {}): GuestRsvpVerif email: "ada@example.org", code: CODE, ...over, - } as GuestRsvpVerifyRequest + } } const ctx = { ip: IP } @@ -497,7 +497,7 @@ describe("guest rsvp: verifying a code", () => { const harness = build({ registrations: { register: (input, subject) => { - calls.push({ idempotencyKey: input.idempotencyKey as string, subject }) + calls.push({ idempotencyKey: input.idempotencyKey, subject }) return Promise.resolve({ outcome: "registered" as const, registration: null, @@ -652,7 +652,7 @@ describe("guest rsvp: verifying a code", () => { channel: "sms", phone: "+15559998888", code: REVIEWER_CODE, - } as GuestRsvpVerifyRequest, + }, ctx, ), ).rejects.toMatchObject({ code: "UNAUTHORIZED" }) @@ -737,7 +737,7 @@ describe("guest rsvp: cancelling", () => { consent: null, slotId: null, source: "self", - idempotencyKey: input.idempotencyKey as string, + idempotencyKey: input.idempotencyKey, waitlistId: null, now: new Date(), }) @@ -837,9 +837,9 @@ describe("guest rsvp: cancelling", () => { }, }) - await expect( - h.service.requestCode(emailRequest({ consent } as never), ctx), - ).rejects.toMatchObject({ code: "VALIDATION" }) + await expect(h.service.requestCode(emailRequest({ consent }), ctx)).rejects.toMatchObject({ + code: "VALIDATION", + }) expect(h.repo.otps).toHaveLength(0) }) @@ -863,9 +863,9 @@ describe("guest rsvp: cancelling", () => { await h.service.requestCode(emailRequest(), ctx) valid = false - await expect( - h.service.verifyCode(emailVerify({ consent } as never), ctx), - ).rejects.toMatchObject({ code: "VALIDATION" }) + await expect(h.service.verifyCode(emailVerify({ consent }), ctx)).rejects.toMatchObject({ + code: "VALIDATION", + }) expect(h.repo.otps[0]?.consumedAt ?? null).toBeNull() expect(h.repo.guests).toHaveLength(0) }) @@ -1108,7 +1108,7 @@ describe("guest rsvp: SMS title truncation", () => { }) await h.service.requestCode(smsRequest(), ctx) await h.service.verifyCode( - { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE } as GuestRsvpVerifyRequest, + { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE }, ctx, ) @@ -1124,7 +1124,7 @@ describe("guest rsvp: SMS title truncation", () => { const h = build({ smsGuestEnabled: true }) await h.service.requestCode(smsRequest(), ctx) await h.service.verifyCode( - { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE } as GuestRsvpVerifyRequest, + { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE }, ctx, ) @@ -1223,7 +1223,7 @@ describe("guest rsvp: one global SMS budget covers every outbound text", () => { await h.service.requestCode(smsRequest(), ctx) await h.service.verifyCode( - { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE } as GuestRsvpVerifyRequest, + { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE }, ctx, ) @@ -1236,7 +1236,7 @@ describe("guest rsvp: one global SMS budget covers every outbound text", () => { await h.service.requestCode(smsRequest(), ctx) const result = await h.service.verifyCode( - { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE } as GuestRsvpVerifyRequest, + { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE }, ctx, ) @@ -1301,7 +1301,7 @@ describe("guest rsvp: the SMS half of the cancel/update notice", () => { const h = build({ smsGuestEnabled: true }) await h.service.requestCode(smsRequest(), ctx) await h.service.verifyCode( - { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE } as GuestRsvpVerifyRequest, + { id: EVENT_ID, channel: "sms", phone: "+15552223333", code: CODE }, ctx, ) h.mailer.sent.length = 0 diff --git a/services/api/test/unit/held-media-moderation-reporter.test.ts b/services/api/test/unit/held-media-moderation-reporter.test.ts index c30c9631..17e53416 100644 --- a/services/api/test/unit/held-media-moderation-reporter.test.ts +++ b/services/api/test/unit/held-media-moderation-reporter.test.ts @@ -37,7 +37,7 @@ describe("held-media moderation item names its reporter", () => { const { db, calls } = stubDb(["trash", "Pile of bags", "Los Angeles", "Dana R", REPORTER_ID]) const repo = makeDrizzleMediaWorkerRepository(db, {} as Sql) - await repo.enqueueHeldModerationItem!({ + await repo.enqueueHeldModerationItem({ reportId: REPORT_ID, reason: "NSFW model over threshold", }) @@ -49,7 +49,7 @@ describe("held-media moderation item names its reporter", () => { const { db, calls } = stubDb(["trash", null, null, null, null]) const repo = makeDrizzleMediaWorkerRepository(db, {} as Sql) - await repo.enqueueHeldModerationItem!({ + await repo.enqueueHeldModerationItem({ reportId: REPORT_ID, reason: "NSFW model over threshold", }) @@ -72,7 +72,7 @@ describe("held media folding into an open moderation item", () => { const db = drizzle(client as never) as unknown as Db const repo = makeDrizzleMediaWorkerRepository(db, {} as Sql) - await repo.enqueueHeldModerationItem!({ + await repo.enqueueHeldModerationItem({ reportId: REPORT_ID, reason: "NSFW model over threshold", }) diff --git a/services/api/test/unit/host-broadcast-audience.test.ts b/services/api/test/unit/host-broadcast-audience.test.ts index 7b31ad8a..c79c74e7 100644 --- a/services/api/test/unit/host-broadcast-audience.test.ts +++ b/services/api/test/unit/host-broadcast-audience.test.ts @@ -2,7 +2,6 @@ import type { BroadcastKind, BroadcastSegment } from "@civfix/shared" import { FakeMailer } from "@civfix/shared/fakes" import { describe, expect, it, vi } from "vitest" import { InMemoryCounterStore } from "../../src/abuse/counter-store.js" -import type { Sql } from "../../src/db/client.js" import { audiencePages } from "../../src/services/host/broadcast-audience.js" import { makeDrizzleBroadcastAudienceRepository } from "../../src/services/host/broadcast-audience-repository.drizzle.js" import { @@ -394,7 +393,7 @@ describe("broadcast preview audience count", () => { describe("broadcast audience count SQL", () => { it("counts each side with one statement that wraps the page statement, capped and from the start", async () => { const rec = makeSqlRecorder() - const repo = makeDrizzleBroadcastAudienceRepository(rec.sql as unknown as Sql) + const repo = makeDrizzleBroadcastAudienceRepository(rec.sql) await repo.audiencePage({ cleanupId: EVENT, segment: { kind: "all_registered" }, @@ -425,7 +424,7 @@ describe("broadcast audience count SQL", () => { it("runs no member statement for guests_only and no guest statement for slots", async () => { const rec = makeSqlRecorder() - const repo = makeDrizzleBroadcastAudienceRepository(rec.sql as unknown as Sql) + const repo = makeDrizzleBroadcastAudienceRepository(rec.sql) rec.enqueue([{ n: 2 }]) const guestsOnly = await repo.audienceCount({ cleanupId: EVENT, diff --git a/services/api/test/unit/host-export-csv.test.ts b/services/api/test/unit/host-export-csv.test.ts index d3ec6328..64e902ca 100644 --- a/services/api/test/unit/host-export-csv.test.ts +++ b/services/api/test/unit/host-export-csv.test.ts @@ -281,7 +281,7 @@ describe("host export build", () => { listExpired: () => Promise.resolve([h.current()]), markExpired: () => Promise.reject(new Error("should not be called")), listOrphaned: () => Promise.resolve([]), - } as HostExportRepository, + }, storage: { put: () => Promise.resolve(), presignGet: () => Promise.resolve("x"), diff --git a/services/api/test/unit/host-keyset-cursor-precision.test.ts b/services/api/test/unit/host-keyset-cursor-precision.test.ts index 77cfe81e..549c97ec 100644 --- a/services/api/test/unit/host-keyset-cursor-precision.test.ts +++ b/services/api/test/unit/host-keyset-cursor-precision.test.ts @@ -221,7 +221,7 @@ async function adminApp(handlers: SqlHandler[]): Promise<{ async function nextCursorOf(app: FastifyInstance, url: string): Promise { const res = await app.inject({ method: "GET", url }) expect(res.statusCode).toBe(200) - return (res.json() as { nextCursor: string | null }).nextCursor + return res.json<{ nextCursor: string | null }>().nextCursor } describe("operator broadcast list cursors", () => { diff --git a/services/api/test/unit/host-team-seat-sql.test.ts b/services/api/test/unit/host-team-seat-sql.test.ts index c84d45d7..a700842b 100644 --- a/services/api/test/unit/host-team-seat-sql.test.ts +++ b/services/api/test/unit/host-team-seat-sql.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest" -import type { Sql } from "../../src/db/client.js" import { makeDrizzleHostTeamRepository } from "../../src/services/host/host-team-repository.drizzle.js" import { makeSqlRecorder, type SqlRecorder } from "../helpers/sql-recorder.js" @@ -33,7 +32,7 @@ function acceptFlow( } async function accept(rec: SqlRecorder) { - return makeDrizzleHostTeamRepository(rec.sql as unknown as Sql).acceptInviteByIdTx({ + return makeDrizzleHostTeamRepository(rec.sql).acceptInviteByIdTx({ inviteId: INVITE, userId: USER, now: NOW, diff --git a/services/api/test/unit/host/cleanup-host-fields.test.ts b/services/api/test/unit/host/cleanup-host-fields.test.ts index ffe56145..1724111b 100644 --- a/services/api/test/unit/host/cleanup-host-fields.test.ts +++ b/services/api/test/unit/host/cleanup-host-fields.test.ts @@ -30,7 +30,7 @@ function base(over: Partial = {}): CreateCleanupRequest { scheduledAt: new Date(Date.now() + 86_400_000).toISOString(), slots: [{ title: "Volunteers" }], ...over, - } as CreateCleanupRequest + } } function makeService(): CleanupService { diff --git a/services/api/test/unit/host/host-routes.test.ts b/services/api/test/unit/host/host-routes.test.ts index ee763bfa..e707bf6e 100644 --- a/services/api/test/unit/host/host-routes.test.ts +++ b/services/api/test/unit/host/host-routes.test.ts @@ -160,7 +160,7 @@ describe("host registration routes", () => { for (const name of names) { const ep = endpoints[name] expect( - app.hasRoute({ method: ep.method as "GET", url: versionedPath(ep) }), + app.hasRoute({ method: ep.method, url: versionedPath(ep) }), `${name} is not registered at ${ep.method} ${versionedPath(ep)}`, ).toBe(true) } diff --git a/services/api/test/unit/host/hosted-event-ids-sql.test.ts b/services/api/test/unit/host/hosted-event-ids-sql.test.ts index 47592e4f..91d2f591 100644 --- a/services/api/test/unit/host/hosted-event-ids-sql.test.ts +++ b/services/api/test/unit/host/hosted-event-ids-sql.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest" -import type { Sql } from "../../../src/db/client.js" import { makeDrizzleAnalyticsRepository } from "../../../src/services/host/analytics-repository.drizzle.js" import { makeSqlRecorder } from "../../helpers/sql-recorder.js" @@ -9,11 +8,7 @@ const ORG = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" async function hostedEventIdsQuery(organizationId: string | null) { const rec = makeSqlRecorder() rec.enqueue([{ id: "b" }, { id: "a" }]) - const ids = await makeDrizzleAnalyticsRepository(rec.sql as unknown as Sql).hostedEventIds( - USER, - organizationId, - 7, - ) + const ids = await makeDrizzleAnalyticsRepository(rec.sql).hostedEventIds(USER, organizationId, 7) expect(rec.queries).toHaveLength(1) return { ids, query: rec.queries[0]! } } diff --git a/services/api/test/unit/host/org-team-routes.test.ts b/services/api/test/unit/host/org-team-routes.test.ts index cfbc1829..c5614a3a 100644 --- a/services/api/test/unit/host/org-team-routes.test.ts +++ b/services/api/test/unit/host/org-team-routes.test.ts @@ -119,7 +119,7 @@ async function makeHarness(): Promise { const setCookies = web.headers["set-cookie"] const cookieList = Array.isArray(setCookies) ? setCookies : [String(setCookies ?? "")] const cookie = cookieList.map((c) => c.split(";")[0]).join("; ") - const csrf = (web.json() as { csrfToken?: string }).csrfToken ?? "" + const csrf = web.json<{ csrfToken?: string }>().csrfToken ?? "" async function signIn(otherEmail: string): Promise { await app.inject({ @@ -134,7 +134,7 @@ async function makeHarness(): Promise { headers: { "x-client": "mobile" }, payload: { email: otherEmail, code: otherCode }, }) - const body = res.json() as { token: string; user: { id: string } } + const body = res.json<{ token: string; user: { id: string } }>() // The org repo's user table is separate from the auth stores: mirror the account so the invite's // email-match rule can see it. orgs.seedUser({ id: body.user.id, displayName: otherEmail, email: otherEmail }) @@ -395,7 +395,7 @@ describe("organization invites + suspension routes (0.41.0)", () => { headers: auth(h.token), payload: { name: "Ballona Creek Trust", slug: "ballona-creek-trust" }, }) - return (created.json() as { id: string }).id + return created.json<{ id: string }>().id } it("emails an invite to an address with no account, lists it, and revokes it", async () => { @@ -408,11 +408,11 @@ describe("organization invites + suspension routes (0.41.0)", () => { payload: { identifierKind: "email", identifier: "newcomer@example.org", role: "member" }, }) expect(invited.statusCode).toBe(200) - const body = invited.json() as { + const body = invited.json<{ member: null invited: boolean invite: { id: string; status: string } - } + }>() expect(body.member).toBeNull() expect(body.invited).toBe(true) expect(body.invite).toMatchObject({ @@ -431,7 +431,7 @@ describe("organization invites + suspension routes (0.41.0)", () => { headers: auth(h.token), }) expect(listed.statusCode).toBe(200) - expect((listed.json() as { items: { id: string }[] }).items.map((i) => i.id)).toEqual([ + expect(listed.json<{ items: { id: string }[] }>().items.map((i) => i.id)).toEqual([ body.invite.id, ]) @@ -477,8 +477,8 @@ describe("organization invites + suspension routes (0.41.0)", () => { }) expect(known.statusCode).toBe(200) expect(unknown.statusCode).toBe(200) - const a = known.json() as { invite: Record } - const b = unknown.json() as { invite: Record } + const a = known.json<{ invite: Record }>() + const b = unknown.json<{ invite: Record }>() expect(Object.keys(a).sort()).toEqual(Object.keys(b).sort()) expect(Object.keys(a.invite).sort()).toEqual(Object.keys(b.invite).sort()) expect(a.invite).toMatchObject({ user: null, status: "pending" }) @@ -494,7 +494,7 @@ describe("organization invites + suspension routes (0.41.0)", () => { payload: { identifierKind: "email", identifier, role: "member" }, }) expect(again.statusCode, identifier).toBe(200) - expect((again.json() as { invite: { status: string } }).invite.status).toBe("pending") + expect(again.json<{ invite: { status: string } }>().invite.status).toBe("pending") } }) @@ -507,14 +507,14 @@ describe("organization invites + suspension routes (0.41.0)", () => { headers: auth(h.token), payload: { name: "Second Org", slug: "second-org" }, }) - const second = (other.json() as { id: string }).id + const second = other.json<{ id: string }>().id const invited = await h.app.inject({ method: "POST", url: `/v1/orgs/${first}/members`, headers: auth(h.token), payload: { identifierKind: "email", identifier: "newcomer@example.org", role: "member" }, }) - const inviteId = (invited.json() as { invite: { id: string } }).invite.id + const inviteId = invited.json<{ invite: { id: string } }>().invite.id const mismatched = await h.app.inject({ method: "DELETE", url: `/v1/orgs/${second}/invites/${inviteId}`, diff --git a/services/api/test/unit/host/organization-access-gate.test.ts b/services/api/test/unit/host/organization-access-gate.test.ts index b2275400..36bd8b61 100644 --- a/services/api/test/unit/host/organization-access-gate.test.ts +++ b/services/api/test/unit/host/organization-access-gate.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest" import { randomUUID } from "node:crypto" import { InMemoryCounterStore } from "../../../src/abuse/counter-store.js" -import type { Sql } from "../../../src/db/client.js" import { makeDrizzleOrganizationRepository } from "../../../src/services/host/organization-repository.drizzle.js" import { makeOrganizationService, @@ -38,9 +37,7 @@ beforeEach(() => { async function seededOrg(): Promise { const dto = await service.createOrganization( - { name: "Ballona Creek Trust", slug: "ballona-creek-trust" } as Parameters< - OrganizationService["createOrganization"] - >[0], + { name: "Ballona Creek Trust", slug: "ballona-creek-trust" }, OWNER, ) for (const [handle, role] of [ @@ -182,9 +179,10 @@ describe("findOrganizationAccess SQL", () => { my_role: "admin", }, ]) - const record = await makeDrizzleOrganizationRepository( - rec.sql as unknown as Sql, - ).findOrganizationAccess("org-1", ADMIN) + const record = await makeDrizzleOrganizationRepository(rec.sql).findOrganizationAccess( + "org-1", + ADMIN, + ) expect(record).toEqual({ id: "org-1", @@ -208,7 +206,7 @@ describe("findOrganizationAccess SQL", () => { it("returns null when no row comes back", async () => { const rec = makeSqlRecorder() - const repository = makeDrizzleOrganizationRepository(rec.sql as unknown as Sql) + const repository = makeDrizzleOrganizationRepository(rec.sql) expect(await repository.findOrganizationAccess("org-1", ADMIN)).toBeNull() }) }) diff --git a/services/api/test/unit/host/organization-service-security.test.ts b/services/api/test/unit/host/organization-service-security.test.ts index b3801b77..b86ae2c1 100644 --- a/services/api/test/unit/host/organization-service-security.test.ts +++ b/services/api/test/unit/host/organization-service-security.test.ts @@ -50,10 +50,7 @@ beforeEach(() => { }) async function orgWithAdmin(slug = "ballona-creek-trust"): Promise { - const dto = await service.createOrganization( - { name: `Org ${slug}`, slug } as Parameters[0], - OWNER, - ) + const dto = await service.createOrganization({ name: `Org ${slug}`, slug }, OWNER) await service.inviteMember(dto.id, OWNER, { identifierKind: "handle", identifier: "adam", diff --git a/services/api/test/unit/host/organization-service.test.ts b/services/api/test/unit/host/organization-service.test.ts index 0ebff881..fdb7b321 100644 --- a/services/api/test/unit/host/organization-service.test.ts +++ b/services/api/test/unit/host/organization-service.test.ts @@ -1248,7 +1248,7 @@ describe("admin org management (0.41.0)", () => { ownerUserId: OWNER, reason: "onboarded at the partner summit", ...over, - } as Parameters[1]) + }) } it("creates an org for an owner, already verified when a kind is given, with audit rows", async () => { diff --git a/services/api/test/unit/jobs-pgboss.test.ts b/services/api/test/unit/jobs-pgboss.test.ts index 86e43a79..ff3f4b01 100644 --- a/services/api/test/unit/jobs-pgboss.test.ts +++ b/services/api/test/unit/jobs-pgboss.test.ts @@ -33,7 +33,7 @@ vi.mock("pg-boss", () => { default: class { constructor(opts: unknown) { ctor(opts) - return lastBoss as unknown as object + return lastBoss } }, } diff --git a/services/api/test/unit/jurisdiction-contacts-upsert-sql.test.ts b/services/api/test/unit/jurisdiction-contacts-upsert-sql.test.ts index a314496f..48cd6fda 100644 --- a/services/api/test/unit/jurisdiction-contacts-upsert-sql.test.ts +++ b/services/api/test/unit/jurisdiction-contacts-upsert-sql.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest" -import type { Queryable } from "../../src/db/client.js" import { upsertJurisdictionContacts } from "../../src/services/admin/jurisdiction-contacts-repository.drizzle.js" import { makeSqlRecorder, type SqlRecorder } from "../helpers/sql-recorder.js" @@ -12,7 +11,7 @@ async function save( contacts: Parameters[2], ): Promise { const rec = makeSqlRecorder() - await upsertJurisdictionContacts(rec.sql as unknown as Queryable, GEOID, contacts, [], null) + await upsertJurisdictionContacts(rec.sql, GEOID, contacts, [], null) return rec } diff --git a/services/api/test/unit/local-storage-routes.test.ts b/services/api/test/unit/local-storage-routes.test.ts index 168fa882..f933834c 100644 --- a/services/api/test/unit/local-storage-routes.test.ts +++ b/services/api/test/unit/local-storage-routes.test.ts @@ -124,10 +124,10 @@ describe("presign -> PUT -> GET round trip", () => { }, }) expect(created.statusCode).toBe(200) - const { putUrl, headers } = created.json() as { + const { putUrl, headers } = created.json<{ putUrl: string headers: Record - } + }>() expect(putUrl.startsWith(`${PUBLIC_API_URL}/_local-storage/media/uploads/`)).toBe(true) const uploaded = await app.inject({ diff --git a/services/api/test/unit/media-byte-quota-window.test.ts b/services/api/test/unit/media-byte-quota-window.test.ts index 8949f7ff..63203ab7 100644 --- a/services/api/test/unit/media-byte-quota-window.test.ts +++ b/services/api/test/unit/media-byte-quota-window.test.ts @@ -12,7 +12,7 @@ const KEY = MEDIA_UPLOAD_BYTE_PREFIX + SUBJECT const ELAPSED_WINDOW_MS = 3_000 function freshRedis(): RedisClient { - return new RedisMock() as unknown as RedisClient + return new RedisMock() } describe("RedisByteMeter keeps a fixed daily window", () => { diff --git a/services/api/test/unit/media-guest-upload-after-sign-in.test.ts b/services/api/test/unit/media-guest-upload-after-sign-in.test.ts index c3ca4cf5..d4b8f098 100644 --- a/services/api/test/unit/media-guest-upload-after-sign-in.test.ts +++ b/services/api/test/unit/media-guest-upload-after-sign-in.test.ts @@ -83,7 +83,7 @@ async function guestUpload(h: AuthHarness, mediaRepo: InMemoryMediaRepository): payload: UPLOAD_REQUEST, }) expect(res.statusCode).toBe(200) - const { uploadId } = res.json() as { uploadId: string } + const { uploadId } = res.json<{ uploadId: string }>() const asset = (await mediaRepo.findByUploadId(uploadId))! expect(asset.uploader).toBe(anonUploader(GUEST_TOKEN_ID)) await (h.container.storage as FakeStorage).put( diff --git a/services/api/test/unit/pg-stores-handle-race.test.ts b/services/api/test/unit/pg-stores-handle-race.test.ts index 7e625f43..5226a1db 100644 --- a/services/api/test/unit/pg-stores-handle-race.test.ts +++ b/services/api/test/unit/pg-stores-handle-race.test.ts @@ -32,7 +32,7 @@ function recordingDb(respond: Responder): { db: Db; statements: Recorded[] } { return callback(client) }, } - return { db: drizzle(client as never, { schema }) as unknown as Db, statements } + return { db: drizzle(client as never, { schema }), statements } } function userRow(fields: Record): unknown[] { diff --git a/services/api/test/unit/pg-stores-security.test.ts b/services/api/test/unit/pg-stores-security.test.ts index 53c36401..b356a566 100644 --- a/services/api/test/unit/pg-stores-security.test.ts +++ b/services/api/test/unit/pg-stores-security.test.ts @@ -41,7 +41,7 @@ function recordingDb(respond: Responder = () => ({})): { db: Db; statements: Rec } }, } - return { db: drizzle(client as never, { schema }) as unknown as Db, statements } + return { db: drizzle(client as never, { schema }), statements } } function erasedUserRow(): unknown[] { diff --git a/services/api/test/unit/redis-incr-window.test.ts b/services/api/test/unit/redis-incr-window.test.ts index 1908c2e7..1e649af3 100644 --- a/services/api/test/unit/redis-incr-window.test.ts +++ b/services/api/test/unit/redis-incr-window.test.ts @@ -7,7 +7,7 @@ const WINDOW_SECONDS = 60 const ELAPSED_WINDOW_MS = 3_000 function freshRedis(): RedisClient { - return new RedisMock() as unknown as RedisClient + return new RedisMock() } describe("atomic counters keep a fixed window", () => { diff --git a/services/api/test/unit/registration-questions-sql.test.ts b/services/api/test/unit/registration-questions-sql.test.ts index 445dd9d7..7250dded 100644 --- a/services/api/test/unit/registration-questions-sql.test.ts +++ b/services/api/test/unit/registration-questions-sql.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest" -import type { Sql } from "../../src/db/client.js" import { makeDrizzleHostRegistrationRepository } from "../../src/services/host/registration-repository.drizzle.js" import type { DesiredQuestion } from "../../src/services/host/registration-repository.js" import { makeSqlRecorder, type ExecutedQuery, type SqlRecorder } from "../helpers/sql-recorder.js" @@ -31,11 +30,7 @@ function question(over: Partial = {}): DesiredQuestion { async function reconcile(desired: DesiredQuestion[]): Promise { const rec = makeSqlRecorder() rec.on(/^SELECT id FROM cleanups/, [{ id: EVENT }]) - await makeDrizzleHostRegistrationRepository(rec.sql as unknown as Sql).reconcileQuestions( - EVENT, - desired, - NOW, - ) + await makeDrizzleHostRegistrationRepository(rec.sql).reconcileQuestions(EVENT, desired, NOW) return rec } @@ -217,9 +212,11 @@ describe("saving an event's questions", () => { it("writes nothing for an event that is gone", async () => { const rec = makeSqlRecorder() - const saved = await makeDrizzleHostRegistrationRepository( - rec.sql as unknown as Sql, - ).reconcileQuestions(EVENT, mixed, NOW) + const saved = await makeDrizzleHostRegistrationRepository(rec.sql).reconcileQuestions( + EVENT, + mixed, + NOW, + ) expect(saved).toEqual([]) expect(rec.queries).toHaveLength(1) diff --git a/services/api/test/unit/registration-sql.test.ts b/services/api/test/unit/registration-sql.test.ts index 8d32d1d8..21060dde 100644 --- a/services/api/test/unit/registration-sql.test.ts +++ b/services/api/test/unit/registration-sql.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest" -import type { Sql } from "../../src/db/client.js" import { makeDrizzleHostRegistrationRepository } from "../../src/services/host/registration-repository.drizzle.js" import { makeSqlRecorder, type SqlRecorder } from "../helpers/sql-recorder.js" @@ -11,7 +10,7 @@ const NOW = new Date("2026-03-04T05:06:07.000Z") const SORT_UPDATE = /^UPDATE cleanup_ticket_types/ function repoOver(rec: SqlRecorder) { - return makeDrizzleHostRegistrationRepository(rec.sql as unknown as Sql) + return makeDrizzleHostRegistrationRepository(rec.sql) } function recorderWithTypes(...ids: string[]): SqlRecorder { diff --git a/services/api/test/unit/report-chat-bell-wiring.test.ts b/services/api/test/unit/report-chat-bell-wiring.test.ts index 71a1e2d6..322cb75c 100644 --- a/services/api/test/unit/report-chat-bell-wiring.test.ts +++ b/services/api/test/unit/report-chat-bell-wiring.test.ts @@ -15,6 +15,7 @@ import { InMemoryDmRepository, } from "../../src/services/dm-repository.memory.js" +// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- the assertion is the field's declared type: vi.hoisted infers the record from this literal const { captured } = vi.hoisted(() => ({ captured: {} as { reportChat?: GatewayReportChat } })) vi.mock("../../src/ws/gateway.js", async (importOriginal) => { @@ -42,15 +43,15 @@ function makeReportChatSource( } return { isMember: () => Promise.resolve(true), - roleOf: notImpl("roleOf") as never, + roleOf: notImpl("roleOf"), advanceReadWatermark, - markRead: notImpl("markRead") as never, - join: notImpl("join") as never, - leave: notImpl("leave") as never, - insertSystemMessage: notImpl("insertSystemMessage") as never, - listMemberIds: notImpl("listMemberIds") as never, - countMembers: notImpl("countMembers") as never, - listMembers: notImpl("listMembers") as never, + markRead: notImpl("markRead"), + join: notImpl("join"), + leave: notImpl("leave"), + insertSystemMessage: notImpl("insertSystemMessage"), + listMemberIds: notImpl("listMemberIds"), + countMembers: notImpl("countMembers"), + listMembers: notImpl("listMembers"), } } diff --git a/services/api/test/unit/report-chat-emitter-seams.test.ts b/services/api/test/unit/report-chat-emitter-seams.test.ts index 8195974b..a9121e4d 100644 --- a/services/api/test/unit/report-chat-emitter-seams.test.ts +++ b/services/api/test/unit/report-chat-emitter-seams.test.ts @@ -25,6 +25,7 @@ import type { ChatMessageDTO } from "@civfix/shared" import type { Container } from "../../src/di.js" const stub = vi.hoisted(() => ({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- the assertion is the field's declared type: vi.hoisted infers the record from this literal notifRepo: undefined as unknown, members: [] as string[], /** `${roomKind}|${roomId}|${userId}` */ diff --git a/services/api/test/unit/report-chat-fanout-wiring.test.ts b/services/api/test/unit/report-chat-fanout-wiring.test.ts index 080fc21b..bfb2b844 100644 --- a/services/api/test/unit/report-chat-fanout-wiring.test.ts +++ b/services/api/test/unit/report-chat-fanout-wiring.test.ts @@ -4,6 +4,7 @@ import type { ChatMessageDTO } from "@civfix/shared" import type { Container } from "../../src/di.js" const stub = vi.hoisted(() => ({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- the assertion is the field's declared type: vi.hoisted infers the record from this literal notifRepo: undefined as unknown, roster: [] as string[], limits: [] as (number | undefined)[], diff --git a/services/api/test/unit/report-chat-membership.test.ts b/services/api/test/unit/report-chat-membership.test.ts index 4f044ecf..90f95efe 100644 --- a/services/api/test/unit/report-chat-membership.test.ts +++ b/services/api/test/unit/report-chat-membership.test.ts @@ -38,12 +38,12 @@ function makeFakeReportChat( join, leave, roleOf: () => Promise.resolve(over.isMember ? ("member" as const) : null), - advanceReadWatermark: notImpl("advanceReadWatermark") as never, - markRead: notImpl("markRead") as never, - insertSystemMessage: notImpl("insertSystemMessage") as never, - listMemberIds: notImpl("listMemberIds") as never, - countMembers: notImpl("countMembers") as never, - listMembers: notImpl("listMembers") as never, + advanceReadWatermark: notImpl("advanceReadWatermark"), + markRead: notImpl("markRead"), + insertSystemMessage: notImpl("insertSystemMessage"), + listMemberIds: notImpl("listMemberIds"), + countMembers: notImpl("countMembers"), + listMembers: notImpl("listMembers"), } } diff --git a/services/api/test/unit/report-service-replay-and-meta.test.ts b/services/api/test/unit/report-service-replay-and-meta.test.ts index cc4cb758..5a15ba0f 100644 --- a/services/api/test/unit/report-service-replay-and-meta.test.ts +++ b/services/api/test/unit/report-service-replay-and-meta.test.ts @@ -16,7 +16,7 @@ function createReq(): CreateReportRequest { lng: -118.25, geomSource: "device", mediaUploadIds: [], - } as CreateReportRequest + } } function makeHarness(over: Partial[0]> = {}) { diff --git a/services/api/test/unit/request-text-hardening.test.ts b/services/api/test/unit/request-text-hardening.test.ts index 56a77b7a..c736d03a 100644 --- a/services/api/test/unit/request-text-hardening.test.ts +++ b/services/api/test/unit/request-text-hardening.test.ts @@ -377,7 +377,7 @@ describe("PATCH /dm/:threadId/messages/:messageId over HTTP (CVX-002b)", () => { headers: { "x-client": "mobile" }, payload: { email: "me@example.com", code: mailer.lastOtpFor("me@example.com")! }, }) - const { token, user } = verified.json() as { token: string; user: { id: string } } + const { token, user } = verified.json<{ token: string; user: { id: string } }>() dmRepo.registerUser({ id: user.id, displayName: "Me", handle: "me" }) const thread = await dmRepo.openOrCreateThread(user.id, PEER) const message = await dmRepo.persist({ threadId: thread.id, senderId: user.id, body: "typo" }) diff --git a/services/api/test/unit/reverse-geocode-mapbox.test.ts b/services/api/test/unit/reverse-geocode-mapbox.test.ts index a25f6053..dc67aed3 100644 --- a/services/api/test/unit/reverse-geocode-mapbox.test.ts +++ b/services/api/test/unit/reverse-geocode-mapbox.test.ts @@ -6,11 +6,11 @@ import { } from "../../src/adapters/reverse-geocode.mapbox.js" function okFetch(props: unknown): typeof fetch { - return (async () => + return async () => ({ ok: true, json: async () => ({ features: [{ properties: props }] }), - }) as unknown as Response) as unknown as typeof fetch + }) as unknown as Response } describe("formatMapboxReverse", () => { @@ -66,27 +66,27 @@ describe("makeMapboxReverseGeocode", () => { it("returns null on non-ok HTTP", async () => { const geocode = makeMapboxReverseGeocode({ token: "pk.test", - fetchImpl: (async () => ({ ok: false }) as unknown as Response) as unknown as typeof fetch, + fetchImpl: async () => ({ ok: false }) as unknown as Response, }) expect(await geocode(39.8, -89.6)).toBeNull() }) it("returns null when fetch throws", async () => { const geocode = makeMapboxReverseGeocode({ token: "pk.test", - fetchImpl: (async () => { + fetchImpl: async () => { throw new Error("net") - }) as unknown as typeof fetch, + }, }) expect(await geocode(39.8, -89.6)).toBeNull() }) it("returns null for empty features", async () => { const geocode = makeMapboxReverseGeocode({ token: "pk.test", - fetchImpl: (async () => + fetchImpl: async () => ({ ok: true, json: async () => ({ features: [] }), - }) as unknown as Response) as unknown as typeof fetch, + }) as unknown as Response, }) expect(await geocode(39.8, -89.6)).toBeNull() }) @@ -94,10 +94,10 @@ describe("makeMapboxReverseGeocode", () => { let called = false const geocode = makeMapboxReverseGeocode({ token: "pk.test", - fetchImpl: (async () => { + fetchImpl: async () => { called = true return { ok: true, json: async () => ({}) } as unknown as Response - }) as unknown as typeof fetch, + }, }) expect(await geocode(Number.NaN, -89.6)).toBeNull() expect(called).toBe(false) diff --git a/services/api/test/unit/reverse-geocode-photon.test.ts b/services/api/test/unit/reverse-geocode-photon.test.ts index df1ce860..ce56d212 100644 --- a/services/api/test/unit/reverse-geocode-photon.test.ts +++ b/services/api/test/unit/reverse-geocode-photon.test.ts @@ -43,11 +43,11 @@ describe("formatPhotonReverse", () => { }) function okFetch(props: unknown): typeof fetch { - return (async () => + return async () => ({ ok: true, json: async () => ({ features: [{ properties: props }] }), - }) as unknown as Response) as unknown as typeof fetch + }) as unknown as Response } describe("makePhotonReverseGeocode", () => { diff --git a/services/api/test/unit/route-coverage.test.ts b/services/api/test/unit/route-coverage.test.ts index 8d8f6273..dbde23a6 100644 --- a/services/api/test/unit/route-coverage.test.ts +++ b/services/api/test/unit/route-coverage.test.ts @@ -221,7 +221,7 @@ describe("route-coverage: every shared endpoint is registered (offline boot smok const res = await app.inject(injectArgs(ep)) if (res.statusCode === 404) { - const body = res.json() as { message?: string } + const body = res.json<{ message?: string }>() const isRouteMissing = typeof body.message === "string" && body.message.startsWith(`Route ${ep.method} `) expect(isRouteMissing, `endpoint ${name} (${ep.method} ${ep.path}) is NOT registered`).toBe( @@ -239,7 +239,7 @@ describe("route-coverage: every shared endpoint is registered (offline boot smok it("the discriminator is not vacuous: a bogus path IS detected as route-missing", async () => { const res = await app.inject({ method: "GET", url: "/this/route/does/not/exist" }) expect(res.statusCode).toBe(404) - const body = res.json() as { message?: string; code?: string } + const body = res.json<{ message?: string; code?: string }>() expect(body.message?.startsWith("Route GET ")).toBe(true) expect(body.code).toBe("NOT_FOUND") }) diff --git a/services/api/test/unit/social-mentions.test.ts b/services/api/test/unit/social-mentions.test.ts index 2bed75df..94733fe9 100644 --- a/services/api/test/unit/social-mentions.test.ts +++ b/services/api/test/unit/social-mentions.test.ts @@ -17,7 +17,7 @@ function fakeSql(rows: unknown[]): Sql { if (Array.isArray(args[0]) && "raw" in (args[0] as object)) return Promise.resolve(rows) return { __fragment: true } }, - }) as Sql + }) } describe("resolveHandles", () => { diff --git a/services/api/test/unit/social-routes.test.ts b/services/api/test/unit/social-routes.test.ts index 3723225e..53fb0191 100644 --- a/services/api/test/unit/social-routes.test.ts +++ b/services/api/test/unit/social-routes.test.ts @@ -400,7 +400,7 @@ describe("GET /users/follow-suggestions", () => { headers: auth(token), }) expect(res.statusCode).toBe(200) - const body = res.json() as { results: Array<{ id: string; isFollowing: boolean }> } + const body = res.json<{ results: Array<{ id: string; isFollowing: boolean }> }>() const ids = body.results.map((p) => p.id) expect(ids[0]).toBe(ORGANIZER) expect(ids).toContain(OTHER) @@ -413,7 +413,7 @@ describe("GET /users/follow-suggestions", () => { url: "/v1/users/follow-suggestions", headers: auth(token), }) - const ids2 = (res2.json() as { results: Array<{ id: string }> }).results.map((p) => p.id) + const ids2 = res2.json<{ results: Array<{ id: string }> }>().results.map((p) => p.id) expect(ids2).not.toContain(ORGANIZER) }) diff --git a/services/api/test/unit/socket-lifecycle-security.test.ts b/services/api/test/unit/socket-lifecycle-security.test.ts index e9fbdb5f..722cb166 100644 --- a/services/api/test/unit/socket-lifecycle-security.test.ts +++ b/services/api/test/unit/socket-lifecycle-security.test.ts @@ -132,7 +132,7 @@ async function openLiveSocket(membership: StalledMembership): Promise { it("returns the stored bytes, or null when absent", async () => { const s = new FakeStorage() await s.put("k", enc.encode("hello")) - expect(new TextDecoder().decode((await s.getObject("k"))!)).toBe("hello") + expect(new TextDecoder().decode(await s.getObject("k"))).toBe("hello") expect(await s.getObject("missing")).toBeNull() }) }) diff --git a/services/api/test/unit/storage-r2-proxy.test.ts b/services/api/test/unit/storage-r2-proxy.test.ts index 5cc2f0fa..3596eb0c 100644 --- a/services/api/test/unit/storage-r2-proxy.test.ts +++ b/services/api/test/unit/storage-r2-proxy.test.ts @@ -100,15 +100,15 @@ describe("R2 client egress", () => { describe("proxy settings parsing", () => { it("reports no proxy when the variable is absent or blank", () => { - expect(readProxySettings({} as NodeJS.ProcessEnv)).toBeNull() - expect(readProxySettings({ HTTPS_PROXY: " " } as NodeJS.ProcessEnv)).toBeNull() + expect(readProxySettings({})).toBeNull() + expect(readProxySettings({ HTTPS_PROXY: " " })).toBeNull() }) it("honors exact, suffix, wildcard and host:port NO_PROXY entries", () => { const settings = readProxySettings({ HTTPS_PROXY: "http://p:8888", NO_PROXY: "postgres, .internal, redis:6379", - } as NodeJS.ProcessEnv)! + })! expect(shouldProxyHost("postgres", settings)).toBe(false) expect(shouldProxyHost("db.internal", settings)).toBe(false) expect(shouldProxyHost("redis", settings)).toBe(false) @@ -117,7 +117,7 @@ describe("proxy settings parsing", () => { const all = readProxySettings({ HTTPS_PROXY: "http://p:8888", NO_PROXY: "*", - } as NodeJS.ProcessEnv)! + })! expect(shouldProxyHost("anything", all)).toBe(false) }) }) diff --git a/services/api/test/unit/thread-read-routes.test.ts b/services/api/test/unit/thread-read-routes.test.ts index 41c6a205..0767f13d 100644 --- a/services/api/test/unit/thread-read-routes.test.ts +++ b/services/api/test/unit/thread-read-routes.test.ts @@ -40,14 +40,14 @@ function fakeReportChat(markRead: ReportChatRepository["markRead"]): ReportChatR return { isMember: () => Promise.resolve(true), roleOf: () => Promise.resolve("member" as const), - join: notImpl("join") as never, - leave: notImpl("leave") as never, - advanceReadWatermark: notImpl("advanceReadWatermark") as never, + join: notImpl("join"), + leave: notImpl("leave"), + advanceReadWatermark: notImpl("advanceReadWatermark"), markRead, - insertSystemMessage: notImpl("insertSystemMessage") as never, - listMemberIds: notImpl("listMemberIds") as never, - countMembers: notImpl("countMembers") as never, - listMembers: notImpl("listMembers") as never, + insertSystemMessage: notImpl("insertSystemMessage"), + listMemberIds: notImpl("listMemberIds"), + countMembers: notImpl("countMembers"), + listMembers: notImpl("listMembers"), } } diff --git a/services/api/test/unit/user-channel-gateway.test.ts b/services/api/test/unit/user-channel-gateway.test.ts index bdee8843..8f423446 100644 --- a/services/api/test/unit/user-channel-gateway.test.ts +++ b/services/api/test/unit/user-channel-gateway.test.ts @@ -334,7 +334,7 @@ describe("registerChatGateway (subscribe-on-handshake / unsubscribe-on-close wir it("an OPEN handshake subscribes the user; the close event disposes the subscription", async () => { const handler = captureGatewayHandler(baseOpts(channel)) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) // The handler's body is an async IIFE (handshake + subscribe are awaited); let it settle. await flush() @@ -357,7 +357,7 @@ describe("registerChatGateway (subscribe-on-handshake / unsubscribe-on-close wir const handler = captureGatewayHandler(baseOpts(racingChannel)) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await flush() // The subscribe is still pending, and the socket closes before the handshake installs its "close" diff --git a/services/api/test/unit/users-routes-security.test.ts b/services/api/test/unit/users-routes-security.test.ts index 547c7114..eae06200 100644 --- a/services/api/test/unit/users-routes-security.test.ts +++ b/services/api/test/unit/users-routes-security.test.ts @@ -35,7 +35,7 @@ async function sessionUserId(h: AuthHarness, token: string): Promise().user?.id ?? null } describe("DELETE /v1/me after the erasure commits", () => { diff --git a/services/api/test/unit/version-gate.test.ts b/services/api/test/unit/version-gate.test.ts index 4befc528..6fd348de 100644 --- a/services/api/test/unit/version-gate.test.ts +++ b/services/api/test/unit/version-gate.test.ts @@ -77,7 +77,7 @@ describe("version gate: dormant policy (v1=current, MIN=v1, nothing deprecated/s // route exists so it is not a route-missing 404 either. expect(res.statusCode).toBe(200) expect(res.json()).toMatchObject({ pong: true }) - const body = res.json() as { code?: string } + const body = res.json<{ code?: string }>() expect(body.code).not.toBe("UNSUPPORTED_API_VERSION") expect(body.code).not.toBe("API_VERSION_SUNSET") }) @@ -85,7 +85,7 @@ describe("version gate: dormant policy (v1=current, MIN=v1, nothing deprecated/s it("rejects an UNKNOWN version /v2/ with 400 UNSUPPORTED_API_VERSION (before route matching)", async () => { const res = await app.inject({ method: "GET", url: "/v2/something" }) expect(res.statusCode).toBe(400) - const body = res.json() as { code?: string; requestId?: string } + const body = res.json<{ code?: string; requestId?: string }>() expect(body.code).toBe("UNSUPPORTED_API_VERSION") // It is the gate, not the not-found handler: a 400, not a route-missing 404. expect(typeof body.requestId).toBe("string") @@ -94,13 +94,13 @@ describe("version gate: dormant policy (v1=current, MIN=v1, nothing deprecated/s it("rejects a far-future UNKNOWN version /v99/x with 400 UNSUPPORTED_API_VERSION", async () => { const res = await app.inject({ method: "GET", url: "/v99/x" }) expect(res.statusCode).toBe(400) - expect((res.json() as { code?: string }).code).toBe("UNSUPPORTED_API_VERSION") + expect(res.json<{ code?: string }>().code).toBe("UNSUPPORTED_API_VERSION") }) it("rejects a BELOW-MIN version /v0/x with 400 UNSUPPORTED_API_VERSION", async () => { const res = await app.inject({ method: "GET", url: "/v0/x" }) expect(res.statusCode).toBe(400) - expect((res.json() as { code?: string }).code).toBe("UNSUPPORTED_API_VERSION") + expect(res.json<{ code?: string }>().code).toBe("UNSUPPORTED_API_VERSION") }) it("bypasses the gate for the unversioned system path /healthz (200, not gate-rejected)", async () => { @@ -131,7 +131,7 @@ describe("version gate: dormant policy (v1=current, MIN=v1, nothing deprecated/s it("ignores a query string when reading the version segment: /v2/x?foo=1 still rejects", async () => { const res = await app.inject({ method: "GET", url: "/v2/x?foo=1" }) expect(res.statusCode).toBe(400) - expect((res.json() as { code?: string }).code).toBe("UNSUPPORTED_API_VERSION") + expect(res.json<{ code?: string }>().code).toBe("UNSUPPORTED_API_VERSION") }) }) diff --git a/services/api/test/unit/volunteer-hours-service.test.ts b/services/api/test/unit/volunteer-hours-service.test.ts index 51c6f3df..464255d9 100644 --- a/services/api/test/unit/volunteer-hours-service.test.ts +++ b/services/api/test/unit/volunteer-hours-service.test.ts @@ -64,7 +64,7 @@ function makeNotifier(throwFor?: string): RecordingNotifier { sent.push({ userId, type: input.type, - vars: (input.vars ?? {}) as Record, + vars: input.vars ?? {}, }) return Promise.resolve({} as Awaited>) }, @@ -1012,7 +1012,7 @@ describe("volunteer hours routes: leaderboard T1/T4 tripwires", () => { headers: { "x-client": "mobile" }, payload: { email, code: mailer.lastOtpFor(email)! }, }) - const body = verify.json() as { token: string; user: { id: string } } + const body = verify.json<{ token: string; user: { id: string } }>() return { app: built, token: body.token, userId: body.user.id } } @@ -1023,7 +1023,7 @@ describe("volunteer hours routes: leaderboard T1/T4 tripwires", () => { url: `/v1/jurisdictions/${GEOID_A}/leaderboard`, }) expect(res.statusCode).toBe(200) - const body = res.json() as { geoid: string; entries: unknown[] } + const body = res.json<{ geoid: string; entries: unknown[] }>() expect(body.geoid).toBe(GEOID_A) expect(body.entries).toEqual([]) }) diff --git a/services/api/test/unit/web-base-url-fallback.test.ts b/services/api/test/unit/web-base-url-fallback.test.ts index 971d1d17..5b95c179 100644 --- a/services/api/test/unit/web-base-url-fallback.test.ts +++ b/services/api/test/unit/web-base-url-fallback.test.ts @@ -80,9 +80,7 @@ describe("links mailed from a runtime with no web origin configured", () => { webOrigin: webBaseUrlOf({ NODE_ENV: "test" }), }) const org = await service.createOrganization( - { name: "Creek Trust", slug: "creek-trust" } as Parameters< - typeof service.createOrganization - >[0], + { name: "Creek Trust", slug: "creek-trust" }, OWNER, ) diff --git a/services/api/test/unit/ws-security-hardening.test.ts b/services/api/test/unit/ws-security-hardening.test.ts index 9e2a2cd3..389b5f6e 100644 --- a/services/api/test/unit/ws-security-hardening.test.ts +++ b/services/api/test/unit/ws-security-hardening.test.ts @@ -124,7 +124,7 @@ describe("H6: presence.leave only reports userGone when it actually removed the }) describe("H6: RedisChatPresence.leave reads the ZREM's own reply slot", () => { - const makeRedis = (): RedisClient => new RedisMock() as unknown as RedisClient + const makeRedis = (): RedisClient => new RedisMock() it("reports userGone:false for a connection that was never in the sorted set", async () => { const redis = makeRedis() diff --git a/services/api/test/unit/ws-socket-lifecycle.test.ts b/services/api/test/unit/ws-socket-lifecycle.test.ts index 84aaf754..ec64f6b7 100644 --- a/services/api/test/unit/ws-socket-lifecycle.test.ts +++ b/services/api/test/unit/ws-socket-lifecycle.test.ts @@ -231,7 +231,7 @@ describe("frames sent during the async handshake are buffered, not dropped", () it("a join sent before the handshake settles is applied once the session exists", async () => { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) await flush() @@ -242,7 +242,7 @@ describe("frames sent during the async handshake are buffered, not dropped", () it("buffered frames are drained IN ORDER (a join lands before the send that followed it)", async () => { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) socket.emit( "message", @@ -259,7 +259,7 @@ describe("frames sent during the async handshake are buffered, not dropped", () it("a REJECTED handshake discards the buffer unread (only the error frame goes out)", async () => { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, anonRequest()) + handler(socket, anonRequest()) socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) await flush() @@ -272,7 +272,7 @@ describe("frames sent during the async handshake are buffered, not dropped", () it("stops buffering past the BYTE ceiling (a legit join behind 64 KiB of junk is dropped)", async () => { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) const filler = JSON.stringify("x".repeat(WS_HANDSHAKE_BUFFER_BYTES / 2 - 2)) expect(Buffer.byteLength(filler)).toBe(WS_HANDSHAKE_BUFFER_BYTES / 2) socket.emit("message", filler) @@ -287,7 +287,7 @@ describe("frames sent during the async handshake are buffered, not dropped", () it("still stops buffering past the frame COUNT, independently of size", async () => { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) for (let i = 0; i < WS_HANDSHAKE_FRAME_BUFFER; i += 1) socket.emit("message", '"x"') socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) await flush() @@ -299,7 +299,7 @@ describe("frames sent during the async handshake are buffered, not dropped", () it("a join that fits under BOTH bounds is still buffered and answered (the caps are not off-by-one)", async () => { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) for (let i = 0; i < WS_HANDSHAKE_FRAME_BUFFER - 1; i += 1) socket.emit("message", '"x"') socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) await flush() @@ -312,7 +312,7 @@ describe("frames sent during the async handshake are buffered, not dropped", () it("frames arriving after the session is live still dispatch (the handover works)", async () => { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await flush() socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) await flush() @@ -329,7 +329,7 @@ describe("the pre-auth buffer holds as many frames as the post-auth queue", () = function pipelineJoins(count: number): MockSocket { const handler = captureGatewayHandler(baseOpts({ isMember: () => Promise.resolve(true) })) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) for (let i = 0; i < count; i += 1) { socket.emit("message", JSON.stringify({ type: "join", cleanupId: roomN(i) })) } @@ -372,7 +372,7 @@ describe("per-user connection cap and slot release", () => { handler: (socket: WebSocket, request: FastifyRequest) => void, ): Promise { const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE, ipN(opened++))) + handler(socket, authedRequest(ALICE, ipN(opened++))) await flush() return socket } @@ -398,22 +398,19 @@ describe("per-user connection cap and slot release", () => { const shared = "203.0.113.44" for (let i = 0; i < MAX_CONNECTIONS_PER_IP; i += 1) { const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(userN(i), shared)) + handler(socket, authedRequest(userN(i), shared)) await flush() expect(socket.closes).toHaveLength(0) } const rejected = new MockSocket() - handler(rejected as unknown as WebSocket, authedRequest(userN(MAX_CONNECTIONS_PER_IP), shared)) + handler(rejected, authedRequest(userN(MAX_CONNECTIONS_PER_IP), shared)) await flush() expect(rejected.framesOfType("error")[0]).toMatchObject({ code: "RATE_LIMITED" }) expect(rejected.closes[0]?.code).toBe(1008) const elsewhere = new MockSocket() - handler( - elsewhere as unknown as WebSocket, - authedRequest(userN(MAX_CONNECTIONS_PER_IP), "203.0.113.45"), - ) + handler(elsewhere, authedRequest(userN(MAX_CONNECTIONS_PER_IP), "203.0.113.45")) await flush() expect(elsewhere.closes).toHaveLength(0) expect(elsewhere.framesOfType("error")).toHaveLength(0) @@ -445,7 +442,7 @@ describe("per-user connection cap and slot release", () => { const handler = captureGatewayHandler(baseOpts({ userChannel: racing })) const dying = new MockSocket() - handler(dying as unknown as WebSocket, authedRequest(ALICE)) + handler(dying, authedRequest(ALICE)) await flush() dying.readyState = WS_CLOSED release!() @@ -488,7 +485,7 @@ describe("socket close DURING an in-flight join", () => { }), ) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await flush() socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) @@ -532,7 +529,7 @@ describe("outbound backpressure (wrapSocket)", () => { it("drops droppable frame types once the outbound buffer is over threshold, keeps messages", () => { const socket = new MockSocket() socket.bufferedAmount = WS_BUFFER_DROP_THRESHOLD + 1 - const conn = wrapSocket(socket as unknown as WebSocket) + const conn = wrapSocket(socket) conn.send(typing) conn.send(JSON.stringify({ type: "presence", cleanupId: ROOM, userId: ALICE, state: "join" })) @@ -548,7 +545,7 @@ describe("outbound backpressure (wrapSocket)", () => { it("drops nothing while the buffer is under threshold", () => { const socket = new MockSocket() socket.bufferedAmount = WS_BUFFER_DROP_THRESHOLD - const conn = wrapSocket(socket as unknown as WebSocket) + const conn = wrapSocket(socket) conn.send(typing) conn.send(message) expect(socket.sent).toHaveLength(2) @@ -557,7 +554,7 @@ describe("outbound backpressure (wrapSocket)", () => { it("classifies on the frame's OWN type, not a 'type' spelled inside user content", () => { const socket = new MockSocket() socket.bufferedAmount = WS_BUFFER_DROP_THRESHOLD + 1 - const conn = wrapSocket(socket as unknown as WebSocket) + const conn = wrapSocket(socket) conn.send( JSON.stringify({ type: "message", @@ -571,7 +568,7 @@ describe("outbound backpressure (wrapSocket)", () => { it("sends nothing on a socket that is no longer OPEN", () => { const socket = new MockSocket() socket.readyState = WS_CLOSED - const conn = wrapSocket(socket as unknown as WebSocket) + const conn = wrapSocket(socket) conn.send(message) expect(socket.sent).toHaveLength(0) }) @@ -581,7 +578,7 @@ describe("outbound backpressure (wrapSocket)", () => { try { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await microflush() const ticks = WS_BUFFER_TERMINATE_TICKS + 3 @@ -598,7 +595,7 @@ describe("outbound backpressure (wrapSocket)", () => { try { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await microflush() socket.bufferedAmount = WS_BUFFER_DROP_THRESHOLD + 1 @@ -621,7 +618,7 @@ describe("outbound backpressure (wrapSocket)", () => { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() socket.respondToPing = false - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await microflush() await vi.advanceTimersByTimeAsync(WS_HEARTBEAT_MS) @@ -640,7 +637,7 @@ describe("outbound backpressure (wrapSocket)", () => { try { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await microflush() socket.bufferedAmount = WS_BUFFER_DROP_THRESHOLD + 1 @@ -688,7 +685,7 @@ describe("send limiter routes each room kind to its own bucket", () => { }), ) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await flush() socket.emit("message", JSON.stringify(frame)) await flush() @@ -776,7 +773,7 @@ describe("F038: post-handshake frames are dispatched in wire order, one at a tim const gate: { release?: () => void } = {} const handler = captureGatewayHandler(baseOpts({ chat: orderedPersistChat(gate) })) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await flush() socket.emit( @@ -810,7 +807,7 @@ describe("F022: the heartbeat re-authorizes joined rooms and evicts a revoked me Promise.resolve(cleanupId === ROOM && userId === ALICE && allowed) const handler = captureGatewayHandler(baseOpts({ isMember })) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await microflush() socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) await microflush() @@ -832,7 +829,7 @@ describe("F022: the heartbeat re-authorizes joined rooms and evicts a revoked me try { const handler = captureGatewayHandler(baseOpts()) const socket = new MockSocket() - handler(socket as unknown as WebSocket, authedRequest(ALICE)) + handler(socket, authedRequest(ALICE)) await microflush() socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) await microflush() @@ -866,7 +863,7 @@ describe("H2: a ?ticket socket is re-validated against session revocation, exact baseOpts({ sessions, redeemTicket: (t) => tickets.redeem(t) }), ) const socket = new MockSocket() - handler(socket as unknown as WebSocket, ticketRequest(ticket)) + handler(socket, ticketRequest(ticket)) await settleUntil(() => vi.getTimerCount() > 0 || socket.closes.length > 0) return { sessions, socket, token } } @@ -930,7 +927,7 @@ describe("H2: a ?ticket socket is re-validated against session revocation, exact baseOpts({ sessions, redeemTicket: (t) => tickets.redeem(t) }), ) const socket = new MockSocket() - handler(socket as unknown as WebSocket, ticketRequest(ticket)) + handler(socket, ticketRequest(ticket)) await settleUntil(() => socket.closes.length > 0) expect(socket.closes).toHaveLength(1) @@ -963,7 +960,7 @@ describe("H3: the cookie socket honours the revocation epoch even when the Redis const handler = captureGatewayHandler(baseOpts({ sessions })) const socket = new MockSocket() - handler(socket as unknown as WebSocket, cookieRequest(ALICE, token)) + handler(socket, cookieRequest(ALICE, token)) await settleUntil(() => vi.getTimerCount() > 0 || socket.closes.length > 0) expect(socket.closes).toHaveLength(0) @@ -995,7 +992,7 @@ describe("NB4: a ticket socket carries a status revalidator so suspension bites baseOpts({ sessions, redeemTicket: (t) => tickets.redeem(t) }), ) const socket = new MockSocket() - handler(socket as unknown as WebSocket, ticketRequest(ticket)) + handler(socket, ticketRequest(ticket)) await settle() expect(socket.closes).toHaveLength(0) @@ -1035,7 +1032,7 @@ describe("NB4: a ticket socket carries a status revalidator so suspension bites baseOpts({ sessions, redeemTicket: (t) => tickets.redeem(t) }), ) const socket = new MockSocket() - handler(socket as unknown as WebSocket, ticketRequest(ticket)) + handler(socket, ticketRequest(ticket)) await settle() socket.emit("message", JSON.stringify({ type: "join", cleanupId: ROOM })) diff --git a/services/media-worker/src/worker.ts b/services/media-worker/src/worker.ts index 513bf46d..f3d7dbb7 100644 --- a/services/media-worker/src/worker.ts +++ b/services/media-worker/src/worker.ts @@ -217,7 +217,7 @@ function requireRepo( console.warn(`${jobName}: no DB configured (offline mode); skipping`) return undefined } - return value as NonNullable + return value } function makeOrphanSweepHandler(seams: WorkerSeams): JobHandler { diff --git a/services/media-worker/test/helpers/pg.ts b/services/media-worker/test/helpers/pg.ts index 8d9a5855..468f3ac4 100644 --- a/services/media-worker/test/helpers/pg.ts +++ b/services/media-worker/test/helpers/pg.ts @@ -105,7 +105,7 @@ export async function withWorkerPg(): Promise { const uri = started.getConnectionUri() // Raw client (full postgres.js serialization) for raw `sql` queries + migrations. - const sql = postgres(uri, { max: 4, onnotice: () => {} }) as Sql + const sql = postgres(uri, { max: 4, onnotice: () => {} }) // Drizzle gets its OWN client so it never clobbers `sql`'s value serializers (see makeDb in // @civfix/api src/db/client.ts and drizzle-orm#3108). Mirrors the API test harness. const drizzleSql = postgres(uri, { max: 2, onnotice: () => {} }) @@ -127,7 +127,7 @@ export async function withWorkerPg(): Promise { let torn = false const harness: WorkerPgHarness = { sql, - db: db as unknown as Db, + db: db, uri, async teardown() { if (torn) return diff --git a/services/media-worker/test/integration/retention-and-phash-pg.test.ts b/services/media-worker/test/integration/retention-and-phash-pg.test.ts index 92bd8983..c30655df 100644 --- a/services/media-worker/test/integration/retention-and-phash-pg.test.ts +++ b/services/media-worker/test/integration/retention-and-phash-pg.test.ts @@ -50,7 +50,7 @@ beforeAll(async () => { USE_FAKE_STORAGE: "1", USE_FAKE_ABUSE_NSFW: "1", USE_FAKE_JOBS: "1", - } as NodeJS.ProcessEnv) + }) }) afterAll(async () => { diff --git a/services/media-worker/test/unit/egress-proxy.test.ts b/services/media-worker/test/unit/egress-proxy.test.ts index 5eb174ae..8d31ae4b 100644 --- a/services/media-worker/test/unit/egress-proxy.test.ts +++ b/services/media-worker/test/unit/egress-proxy.test.ts @@ -24,7 +24,7 @@ function fakeStorage(): Storage { put: () => Promise.resolve(), list: () => Promise.resolve({ keys: [] }), getObject: () => Promise.resolve(null), - } as unknown as Storage + } } function okResponse(): Response { @@ -40,7 +40,7 @@ let globalFetch: typeof fetch beforeEach(() => { undiciFetch.mockReset() EnvHttpProxyAgent.mockReset() - globalFetch = vi.fn(() => Promise.resolve(okResponse())) as unknown as typeof fetch + globalFetch = vi.fn(() => Promise.resolve(okResponse())) vi.stubGlobal("fetch", globalFetch) }) diff --git a/services/media-worker/test/unit/image-lane.test.ts b/services/media-worker/test/unit/image-lane.test.ts index e9913765..00a956db 100644 --- a/services/media-worker/test/unit/image-lane.test.ts +++ b/services/media-worker/test/unit/image-lane.test.ts @@ -122,7 +122,7 @@ describe("image-lane child protocol", () => { written.push(String(chunk)) return true }) as typeof process.stdout.write) - const errSpy = vi.spyOn(process.stderr, "write").mockImplementation((() => true) as never) + const errSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true) const okCode = await laneMain.main([ "node", diff --git a/services/media-worker/test/unit/preflight-proof.test.ts b/services/media-worker/test/unit/preflight-proof.test.ts index a1833bcf..76ee8535 100644 --- a/services/media-worker/test/unit/preflight-proof.test.ts +++ b/services/media-worker/test/unit/preflight-proof.test.ts @@ -129,9 +129,7 @@ describe("sandboxProofFailure", () => { describe("assertSandboxPreflight", () => { it("is a no-op outside production", async () => { - await expect( - assertSandboxPreflight({ NODE_ENV: "development" } as NodeJS.ProcessEnv), - ).resolves.toBeUndefined() + await expect(assertSandboxPreflight({ NODE_ENV: "development" })).resolves.toBeUndefined() }) it("refuses production without the sandbox identity", async () => { @@ -140,7 +138,7 @@ describe("assertSandboxPreflight", () => { NODE_ENV: "production", FFMPEG_PATH: "/bin/sh", FFPROBE_PATH: "/bin/sh", - } as NodeJS.ProcessEnv), + }), ).rejects.toThrow() }) }) diff --git a/services/media-worker/test/unit/sandbox-hardening.test.ts b/services/media-worker/test/unit/sandbox-hardening.test.ts index 00ce6587..50bee0ea 100644 --- a/services/media-worker/test/unit/sandbox-hardening.test.ts +++ b/services/media-worker/test/unit/sandbox-hardening.test.ts @@ -138,19 +138,15 @@ describe("H7: the decoder child inherits nothing", () => { describe("H7: MEDIA_SANDBOX_UID/GID are required in production", () => { it("throws when production leaves them unset", () => { - expect(() => loadSandboxIdentity({ NODE_ENV: "production" } as NodeJS.ProcessEnv)).toThrow( - /MEDIA_SANDBOX_UID/, - ) + expect(() => loadSandboxIdentity({ NODE_ENV: "production" })).toThrow(/MEDIA_SANDBOX_UID/) }) it("throws when only one of the pair is set, in any environment", () => { - expect(() => loadSandboxIdentity({ MEDIA_SANDBOX_UID: "1001" } as NodeJS.ProcessEnv)).toThrow( - /must be set together/, - ) + expect(() => loadSandboxIdentity({ MEDIA_SANDBOX_UID: "1001" })).toThrow(/must be set together/) }) it("returns null outside production when neither is set", () => { - expect(loadSandboxIdentity({ NODE_ENV: "development" } as NodeJS.ProcessEnv)).toBeNull() + expect(loadSandboxIdentity({ NODE_ENV: "development" })).toBeNull() }) it("refuses a production boot with no sandbox identity", async () => { @@ -159,14 +155,12 @@ describe("H7: MEDIA_SANDBOX_UID/GID are required in production", () => { NODE_ENV: "production", FFMPEG_PATH: "/usr/local/bin/ffmpeg", FFPROBE_PATH: "/usr/local/bin/ffprobe", - } as NodeJS.ProcessEnv), + }), ).rejects.toThrow() }) it("is a no-op outside production", async () => { - await expect( - assertSandboxPreflight({ NODE_ENV: "development" } as NodeJS.ProcessEnv), - ).resolves.toBeUndefined() + await expect(assertSandboxPreflight({ NODE_ENV: "development" })).resolves.toBeUndefined() }) }) @@ -292,21 +286,21 @@ describe("B3'': a decoder that could not be STARTED is infra, not a verdict on t describe("the image-lane entry is resolved from the running dist, not from a bundle-relative path", () => { it("honours MEDIA_IMAGE_LANE_ENTRY", () => { - expect( - loadImageLaneEntry({ MEDIA_IMAGE_LANE_ENTRY: "/opt/x/image-lane.js" } as NodeJS.ProcessEnv), - ).toBe("/opt/x/image-lane.js") + expect(loadImageLaneEntry({ MEDIA_IMAGE_LANE_ENTRY: "/opt/x/image-lane.js" })).toBe( + "/opt/x/image-lane.js", + ) }) it("defaults to image-lane.js beside the running entry file", () => { - expect(loadImageLaneEntry({} as NodeJS.ProcessEnv)).toMatch(/image-lane\.js$/) + expect(loadImageLaneEntry({})).toMatch(/image-lane\.js$/) }) }) describe("H8: the production binaries come from the image, never from npm", () => { it("refuses to resolve ffmpeg/ffprobe in production without the env paths", async () => { - await expect( - resolveMediaToolPaths({ NODE_ENV: "production" } as NodeJS.ProcessEnv), - ).rejects.toThrow(/FFMPEG_PATH|FFPROBE_PATH/) + await expect(resolveMediaToolPaths({ NODE_ENV: "production" })).rejects.toThrow( + /FFMPEG_PATH|FFPROBE_PATH/, + ) }) it("refuses a configured path that is not executable", async () => { @@ -315,7 +309,7 @@ describe("H8: the production binaries come from the image, never from npm", () = NODE_ENV: "production", FFMPEG_PATH: "/nonexistent/ffmpeg", FFPROBE_PATH: "/nonexistent/ffprobe", - } as NodeJS.ProcessEnv), + }), ).rejects.toThrow(/not an executable file/) }) @@ -324,12 +318,12 @@ describe("H8: the production binaries come from the image, never from npm", () = NODE_ENV: "production", FFMPEG_PATH: "/bin/sh", FFPROBE_PATH: "/bin/sh", - } as NodeJS.ProcessEnv) + }) expect(paths).toEqual({ ffmpeg: "/bin/sh", ffprobe: "/bin/sh" }) }) it("falls back to the vendored npm binaries outside production", async () => { - const paths = await resolveMediaToolPaths({ NODE_ENV: "test" } as NodeJS.ProcessEnv) + const paths = await resolveMediaToolPaths({ NODE_ENV: "test" }) expect(paths.ffprobe).toMatch(/ffprobe/) expect(paths.ffmpeg).toMatch(/ffmpeg/) }) diff --git a/services/media-worker/test/unit/video-caps.test.ts b/services/media-worker/test/unit/video-caps.test.ts index 2044bc10..48aed772 100644 --- a/services/media-worker/test/unit/video-caps.test.ts +++ b/services/media-worker/test/unit/video-caps.test.ts @@ -13,6 +13,7 @@ type StubProbe = { } const stub = vi.hoisted(() => ({ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- the assertion is the field's declared type: vi.hoisted infers the record from this literal probe: { durationSec: 10, codec: "h264", diff --git a/services/media-worker/test/unit/worker.test.ts b/services/media-worker/test/unit/worker.test.ts index f4099669..d6f4e0f4 100644 --- a/services/media-worker/test/unit/worker.test.ts +++ b/services/media-worker/test/unit/worker.test.ts @@ -304,9 +304,9 @@ describe("F25: post-success hold-release hook is gated on anon+held report state describe("makeSeams production storage guard", () => { it("THROWS when USE_FAKE_STORAGE is on in production (would silently lose media, issue #39)", async () => { - await expect( - makeSeams({ NODE_ENV: "production", USE_FAKE_STORAGE: "1" } as NodeJS.ProcessEnv), - ).rejects.toThrow(/USE_FAKE_STORAGE must be 0 in production/) + await expect(makeSeams({ NODE_ENV: "production", USE_FAKE_STORAGE: "1" })).rejects.toThrow( + /USE_FAKE_STORAGE must be 0 in production/, + ) }) it("fires on fake storage even when USE_FAKE_ABUSE_NSFW is off (abuse seam is not what is guarded)", async () => { @@ -315,7 +315,7 @@ describe("makeSeams production storage guard", () => { NODE_ENV: "production", USE_FAKE_STORAGE: "1", USE_FAKE_ABUSE_NSFW: "0", - } as NodeJS.ProcessEnv), + }), ).rejects.toThrow(/USE_FAKE_STORAGE/) }) }) From 82429939bcf4b0ddb50ac41f45c0743399212c71 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 23 Sep 2026 20:30:10 +0000 Subject: [PATCH 2/6] fix or justify the sites the typed lint rules flag --- services/api/src/adapters/inbound-mail.cf.ts | 2 ++ services/api/src/db/backfill-population-core.ts | 1 + services/api/src/db/demo-join-event.ts | 1 + services/api/src/db/ingest-jurisdictions.ts | 2 +- services/api/src/db/seed-demo-la.ts | 2 ++ services/api/src/env.ts | 9 +++------ services/api/src/errors/http-mapper.ts | 1 + services/api/src/lib/timeout.ts | 1 + .../api/src/services/admin/admin-event-helpers.ts | 2 +- .../services/admin/discovery-repository.drizzle.ts | 4 ++-- .../api/src/services/admin/discovery-repository.ts | 2 +- services/api/src/services/guest-rsvp-service.ts | 2 +- .../api/src/services/host/registration-repository.ts | 4 ++-- services/api/src/services/host/registration-sql.ts | 2 +- .../api/src/services/threads-repository.drizzle.ts | 2 +- .../test/helpers/admin/discovery-repository.memory.ts | 4 ++-- services/api/test/helpers/query.ts | 1 + services/api/test/helpers/sql-predicate.ts | 1 + services/api/test/unit/admin-auth-guard.test.ts | 5 +++-- .../api/test/unit/host-broadcast-pipeline.test.ts | 10 ++++++++-- .../test/unit/host-broadcast-send-correctness.test.ts | 1 + .../api/test/unit/jurisdiction-lookup-census.test.ts | 11 ++++++----- services/api/test/unit/report-verification.test.ts | 8 ++++---- services/api/test/unit/sms-twilio.test.ts | 1 + services/media-worker/src/download.ts | 3 ++- services/media-worker/src/sandbox/exec.ts | 1 + services/media-worker/src/worker-jobs.ts | 1 + services/media-worker/test/unit/pg-boss-jobs.test.ts | 9 +++++---- 28 files changed, 57 insertions(+), 36 deletions(-) diff --git a/services/api/src/adapters/inbound-mail.cf.ts b/services/api/src/adapters/inbound-mail.cf.ts index e46f2a73..1351145d 100644 --- a/services/api/src/adapters/inbound-mail.cf.ts +++ b/services/api/src/adapters/inbound-mail.cf.ts @@ -320,9 +320,11 @@ function stringifyHeader(value: unknown): string { try { return JSON.stringify(value) } catch { + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- last resort for a header object JSON cannot encode; mailparser documents no shape for it return String(value) } } + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- only primitives reach here; objects returned above return String(value) } diff --git a/services/api/src/db/backfill-population-core.ts b/services/api/src/db/backfill-population-core.ts index ffab7c1b..6028af6b 100644 --- a/services/api/src/db/backfill-population-core.ts +++ b/services/api/src/db/backfill-population-core.ts @@ -72,6 +72,7 @@ export function parseAcs(rows: unknown[][]): { geoid: string; population: number if (!Array.isArray(row)) continue const pop = Number(row[varIdx]) if (!Number.isFinite(pop) || pop < 0) continue + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- ACS API cells are JSON scalars, read the same way as the header row const geoid = geoCols.map((i) => String(row[i] ?? "")).join("") if (geoid) out.push({ geoid, population: Math.round(pop) }) } diff --git a/services/api/src/db/demo-join-event.ts b/services/api/src/db/demo-join-event.ts index c1b5d753..57bbfbcd 100644 --- a/services/api/src/db/demo-join-event.ts +++ b/services/api/src/db/demo-join-event.ts @@ -257,6 +257,7 @@ async function main(): Promise { const outcome = await sql .begin(async (tx) => { const joined = await joinDemoUsers(tx, { eventRef, count, hashFor }) + // eslint-disable-next-line @typescript-eslint/only-throw-error -- a unique sentinel, matched by identity in the catch below, that makes sql.begin roll the rehearsal back if (!commit) throw ROLLBACK return joined }) diff --git a/services/api/src/db/ingest-jurisdictions.ts b/services/api/src/db/ingest-jurisdictions.ts index fa57799a..ba2f5cc5 100644 --- a/services/api/src/db/ingest-jurisdictions.ts +++ b/services/api/src/db/ingest-jurisdictions.ts @@ -25,7 +25,7 @@ export type { IngestRow } from "./ingest-jurisdictions-core.js" async function main(): Promise { const file = process.argv[2] - const defaultLayer = (process.argv[3] ?? DEFAULT_LAYER) as IngestRow["layer"] + const defaultLayer: string = process.argv[3] ?? DEFAULT_LAYER const geoidPrefix = (process.argv[4] ?? "").trim() if (!file) { console.error( diff --git a/services/api/src/db/seed-demo-la.ts b/services/api/src/db/seed-demo-la.ts index 7e00367b..a3102d64 100644 --- a/services/api/src/db/seed-demo-la.ts +++ b/services/api/src/db/seed-demo-la.ts @@ -1873,6 +1873,7 @@ async function runPurge(sql: Sql, commit: boolean): Promise { const result = await sql .begin(async (tx) => { const counts = await purgeDemo(tx) + // eslint-disable-next-line @typescript-eslint/only-throw-error -- a unique sentinel, matched by identity in the catch below, that makes sql.begin roll the rehearsal back if (!commit) throw ROLLBACK return counts }) @@ -1952,6 +1953,7 @@ async function runSeed( await writeAll(tx, data) console.log("verifying...") const lines = await verify(tx, now) + // eslint-disable-next-line @typescript-eslint/only-throw-error -- a unique sentinel, matched by identity in the catch below, that makes sql.begin roll the rehearsal back if (!opts.commit) throw ROLLBACK return lines }) diff --git a/services/api/src/env.ts b/services/api/src/env.ts index 5a2d31aa..d5166331 100644 --- a/services/api/src/env.ts +++ b/services/api/src/env.ts @@ -33,7 +33,7 @@ const TILES_BOUNDS_DEFAULT: [number, number, number, number] = [-125, 24, -66, 5 const NODE_ENVS = ["development", "test", "production"] as const const NodeEnvSchema = z.enum(NODE_ENVS).default("development") -const OPTIONAL_STRING_KEYS: ReadonlyArray = [ +const OPTIONAL_STRING_KEYS: ReadonlyArray = [ "R2_INBOUND_BUCKET", "R2_PUBLIC_BASE", "TILES_RASTER_URL", @@ -227,10 +227,7 @@ function checkReviewerBypass( } } -function optionalStrings( - source: NodeJS.ProcessEnv, - keys: ReadonlyArray, -): Partial { +function optionalStrings(source: NodeJS.ProcessEnv, keys: ReadonlyArray): Partial { const out: Record = {} for (const key of keys) { const raw = source[key] @@ -238,7 +235,7 @@ function optionalStrings( out[key] = raw.trim() } } - return out as Partial + return out } let cached: Env | undefined diff --git a/services/api/src/errors/http-mapper.ts b/services/api/src/errors/http-mapper.ts index 8c3ac4a7..f42b4a7e 100644 --- a/services/api/src/errors/http-mapper.ts +++ b/services/api/src/errors/http-mapper.ts @@ -67,6 +67,7 @@ function fieldsFromValidation(err: FastifyError): Record { const path = (v.instancePath || "").split("/").filter(Boolean) const key = path.length > 0 ? path[path.length - 1]! : (v.params?.missingProperty ?? ROOT_FIELD_KEY) + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- ajv sets missingProperty to a string; String() only covers the unknown-typed params bag out[String(key)] = v.message ?? FIELD_INVALID_FALLBACK_MESSAGE } return out diff --git a/services/api/src/lib/timeout.ts b/services/api/src/lib/timeout.ts index bb8c72f7..2e45d9c6 100644 --- a/services/api/src/lib/timeout.ts +++ b/services/api/src/lib/timeout.ts @@ -37,6 +37,7 @@ export function settleWithin(p: Promise, ms: number, hooks: SettleWithinHo }, (err: unknown) => { clearTimeout(timer) + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- passThroughRejection deliberately re-rejects with the work's own reason, Error or not reject(normalize(err)) }, ) diff --git a/services/api/src/services/admin/admin-event-helpers.ts b/services/api/src/services/admin/admin-event-helpers.ts index f87bee4f..7f43a50e 100644 --- a/services/api/src/services/admin/admin-event-helpers.ts +++ b/services/api/src/services/admin/admin-event-helpers.ts @@ -38,7 +38,7 @@ export function eventStatusNote(status: EventStatus): string { case "cancelled": return "Event cancelled" default: - return `Status set to ${status}` + return `Status set to ${String(status)}` } } diff --git a/services/api/src/services/admin/discovery-repository.drizzle.ts b/services/api/src/services/admin/discovery-repository.drizzle.ts index bc413541..b3cf5363 100644 --- a/services/api/src/services/admin/discovery-repository.drizzle.ts +++ b/services/api/src/services/admin/discovery-repository.drizzle.ts @@ -418,9 +418,9 @@ async function loadGeometry( sql: Queryable, taskId: string, geoid: string, -): Promise<{ placeGeojson: unknown | null; center: [number, number] | null; zoom: number | null }> { +): Promise<{ placeGeojson: unknown; center: [number, number] | null; zoom: number | null }> { const [taskRows, centerRows] = await Promise.all([ - sql<{ place_geojson: unknown | null }[]>` + sql<{ place_geojson: unknown }[]>` SELECT place_geojson FROM jurisdiction_discovery_tasks WHERE id = ${taskId} LIMIT 1 `, sql<{ lat: number | null; lng: number | null }[]>` diff --git a/services/api/src/services/admin/discovery-repository.ts b/services/api/src/services/admin/discovery-repository.ts index c5fedbc0..a817f412 100644 --- a/services/api/src/services/admin/discovery-repository.ts +++ b/services/api/src/services/admin/discovery-repository.ts @@ -44,7 +44,7 @@ export interface DiscoveryContactSuggestionRecord { export interface DiscoveryDetailRecord { task: DiscoveryTaskRecord contacts: DiscoveryContactRecord[] - placeGeojson: unknown | null + placeGeojson: unknown samplePins: DiscoverySamplePinRecord[] center: [number, number] | null zoom: number | null diff --git a/services/api/src/services/guest-rsvp-service.ts b/services/api/src/services/guest-rsvp-service.ts index 8c6689d2..01e9f4de 100644 --- a/services/api/src/services/guest-rsvp-service.ts +++ b/services/api/src/services/guest-rsvp-service.ts @@ -552,7 +552,7 @@ export function makeGuestRsvpService(deps: GuestRsvpServiceDeps): GuestRsvpServi ) } - async function mapDeliveryError(err: unknown, phone: string | null): Promise { + async function mapDeliveryError(err: unknown, phone: string | null): Promise { const kind = smsFailureKind(err) if (kind === "opted_out") { if (phone !== null) { diff --git a/services/api/src/services/host/registration-repository.ts b/services/api/src/services/host/registration-repository.ts index 9ab2fbde..554eb775 100644 --- a/services/api/src/services/host/registration-repository.ts +++ b/services/api/src/services/host/registration-repository.ts @@ -122,14 +122,14 @@ export interface DesiredQuestion { export interface AnswerWrite { questionId: string valueText: string | null - valueJson: unknown | null + valueJson: unknown } export interface AnswerRecord { questionId: string prompt: string valueText: string | null - valueJson: unknown | null + valueJson: unknown scrubbedAt: Date | null } diff --git a/services/api/src/services/host/registration-sql.ts b/services/api/src/services/host/registration-sql.ts index ebf4f10a..faf5d429 100644 --- a/services/api/src/services/host/registration-sql.ts +++ b/services/api/src/services/host/registration-sql.ts @@ -312,7 +312,7 @@ export interface AnswerRowSelect { question_id: string prompt: string value_text: string | null - value_json: unknown | null + value_json: unknown scrubbed_at: Date | null } diff --git a/services/api/src/services/threads-repository.drizzle.ts b/services/api/src/services/threads-repository.drizzle.ts index b959f0e7..8a62d4a7 100644 --- a/services/api/src/services/threads-repository.drizzle.ts +++ b/services/api/src/services/threads-repository.drizzle.ts @@ -13,7 +13,7 @@ import { } from "./threads-service.js" import type { ThreadAggregate, ThreadsRepository } from "./threads-repository.js" -function reportThreadTitle(category: ReportCategory | string, addr: string | null): string { +function reportThreadTitle(category: string, addr: string | null): string { const label = REPORT_CATEGORY_LABELS[category as ReportCategory] ?? category const short = (addr ?? "").split(",")[0]?.trim() ?? "" return short !== "" ? `${label} - ${short}` : label diff --git a/services/api/test/helpers/admin/discovery-repository.memory.ts b/services/api/test/helpers/admin/discovery-repository.memory.ts index fbdef9ae..97803f8f 100644 --- a/services/api/test/helpers/admin/discovery-repository.memory.ts +++ b/services/api/test/helpers/admin/discovery-repository.memory.ts @@ -19,7 +19,7 @@ import type { JurisdictionLayer, ReportCategory } from "@civfix/shared" export interface SeededDiscoveryTask { task: DiscoveryTaskRecord contacts: DiscoveryContactRecord[] - placeGeojson: unknown | null + placeGeojson: unknown samplePins: DiscoverySamplePinRecord[] center: [number, number] | null zoom: number | null @@ -53,7 +53,7 @@ export class InMemoryDiscoveryRepository implements DiscoveryRepository { contactCategories?: ReportCategory[] hasDefaultContact?: boolean contacts?: DiscoveryContactRecord[] - placeGeojson?: unknown | null + placeGeojson?: unknown samplePins?: DiscoverySamplePinRecord[] center?: [number, number] | null zoom?: number | null diff --git a/services/api/test/helpers/query.ts b/services/api/test/helpers/query.ts index cf3fb2d2..d3b34c54 100644 --- a/services/api/test/helpers/query.ts +++ b/services/api/test/helpers/query.ts @@ -19,6 +19,7 @@ export function clientQuery(query: Record): string { } else if (typeof value === "object") { sp.append(key, JSON.stringify(value)) } else { + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- only primitives reach here; objects are JSON-encoded above sp.append(key, String(value)) } } diff --git a/services/api/test/helpers/sql-predicate.ts b/services/api/test/helpers/sql-predicate.ts index f52285b6..56404ec8 100644 --- a/services/api/test/helpers/sql-predicate.ts +++ b/services/api/test/helpers/sql-predicate.ts @@ -9,6 +9,7 @@ export function evalSqlPredicate(text: string, row: PredicateRow): boolean { .replace(/(?=])=(?!=)/g, "===") .replace(/\bOR\b/g, "||") .replace(/\bAND\b/g, "&&") + // eslint-disable-next-line @typescript-eslint/no-implied-eval -- compiling the translated SQL predicate is this helper's purpose; the input is test-authored SQL const compiled = new Function("row", `"use strict"; return Boolean(${js})`) as ( candidate: PredicateRow, ) => boolean diff --git a/services/api/test/unit/admin-auth-guard.test.ts b/services/api/test/unit/admin-auth-guard.test.ts index 3127dd0f..10aca130 100644 --- a/services/api/test/unit/admin-auth-guard.test.ts +++ b/services/api/test/unit/admin-auth-guard.test.ts @@ -212,11 +212,11 @@ describe("H2: operator authority is re-checked against ADMIN_EMAILS on EVERY adm headers: { authorization: `Bearer ${token}` }, }) expect(res.statusCode).toBe(200) - const body = res.json() as { + const body = res.json<{ authenticated: boolean operator?: { email: string } csrfToken?: string - } + }>() expect(body.authenticated).toBe(true) expect(body.operator?.email).toBe(ALLOWED) expect(body.csrfToken).toBeTruthy() @@ -395,6 +395,7 @@ describe("POST /admin/auth/logout: idempotent cookie clearing + CSRF enforcement function clearedCookies(res: { headers: Record }): string[] { const raw = res.headers["set-cookie"] + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- a single set-cookie header value is a string const list = raw === undefined ? [] : Array.isArray(raw) ? raw.map(String) : [String(raw)] return list .filter((c) => /Max-Age=0|Expires=Thu, 01 Jan 1970/i.test(c)) diff --git a/services/api/test/unit/host-broadcast-pipeline.test.ts b/services/api/test/unit/host-broadcast-pipeline.test.ts index a8cc0014..bc761143 100644 --- a/services/api/test/unit/host-broadcast-pipeline.test.ts +++ b/services/api/test/unit/host-broadcast-pipeline.test.ts @@ -190,13 +190,13 @@ describe("broadcast plan", () => { const markPlanned = h.repo.markPlanned.bind(h.repo) let crashed = false - h.repo.markPlanned = ((broadcastId: string, args: Parameters[1]) => { + h.repo.markPlanned = (broadcastId: string, args: Parameters[1]) => { if (!crashed) { crashed = true return Promise.reject(new Error("crashed between the last insert and markPlanned")) } return markPlanned(broadcastId, args) - }) as typeof h.repo.markPlanned + } await expect(h.pipeline.plan(id)).rejects.toThrow(/crashed/) expect(h.repo.allDeliveries().filter((d) => d.chunkNo === 2)).toHaveLength(1) @@ -333,6 +333,7 @@ describe("broadcast chunk", () => { sendOutbound: (email: OutboundEmail): Promise => { if (failNext) { failNext = false + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- the classifier under test must handle nodemailer's SMTP-shaped rejection as given return Promise.reject({ responseCode: 451, response: "451 try later" }) } return Promise.resolve({ messageId: email.messageId ?? "" }) @@ -352,6 +353,7 @@ describe("broadcast chunk", () => { const alwaysFails: Mailer = { sendOtp: () => Promise.resolve(), sendTransactional: () => Promise.resolve(), + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- the classifier under test must handle nodemailer's SMTP-shaped rejection as given sendOutbound: () => Promise.reject({ responseCode: 451 }), } const h = harness({ members: 1, mailer: alwaysFails as unknown as FakeMailer }) @@ -367,6 +369,7 @@ describe("broadcast chunk", () => { const bounces: Mailer = { sendOtp: () => Promise.resolve(), sendTransactional: () => Promise.resolve(), + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- the classifier under test must handle nodemailer's SMTP-shaped rejection as given sendOutbound: () => Promise.reject({ responseCode: 550, response: "550 no such user" }), } const h = harness({ members: 1, mailer: bounces as unknown as FakeMailer }) @@ -388,6 +391,7 @@ describe("broadcast chunk", () => { const badAuth: Mailer = { sendOtp: () => Promise.resolve(), sendTransactional: () => Promise.resolve(), + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- the classifier under test must handle nodemailer's SMTP-shaped rejection as given sendOutbound: () => Promise.reject({ code: "EAUTH" }), } const h = harness({ members: 2, mailer: badAuth as unknown as FakeMailer }) @@ -403,6 +407,7 @@ describe("broadcast chunk", () => { const badAuth: Mailer = { sendOtp: () => Promise.resolve(), sendTransactional: () => Promise.resolve(), + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- the classifier under test must handle nodemailer's SMTP-shaped rejection as given sendOutbound: () => Promise.reject({ code: "EAUTH", response: "535 bad creds" }), } const h = harness({ members: 2, mailer: badAuth as unknown as FakeMailer }) @@ -426,6 +431,7 @@ describe("broadcast chunk", () => { const badAuth: Mailer = { sendOtp: () => Promise.resolve(), sendTransactional: () => Promise.resolve(), + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- the classifier under test must handle nodemailer's SMTP-shaped rejection as given sendOutbound: () => Promise.reject({ code: "EAUTH" }), } const h = harness({ members: 1, mailer: badAuth as unknown as FakeMailer }) diff --git a/services/api/test/unit/host-broadcast-send-correctness.test.ts b/services/api/test/unit/host-broadcast-send-correctness.test.ts index 55d6ffbd..4a1fe68d 100644 --- a/services/api/test/unit/host-broadcast-send-correctness.test.ts +++ b/services/api/test/unit/host-broadcast-send-correctness.test.ts @@ -237,6 +237,7 @@ describe("hard-bounce suppression write failure", () => { const bounces: Mailer = { sendOtp: () => Promise.resolve(), sendTransactional: () => Promise.resolve(), + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- the classifier under test must handle nodemailer's SMTP-shaped rejection as given sendOutbound: () => Promise.reject({ responseCode: 550, response: "550 no such user" }), } const logger = logSpy() diff --git a/services/api/test/unit/jurisdiction-lookup-census.test.ts b/services/api/test/unit/jurisdiction-lookup-census.test.ts index 2c1e65c2..fdd17f97 100644 --- a/services/api/test/unit/jurisdiction-lookup-census.test.ts +++ b/services/api/test/unit/jurisdiction-lookup-census.test.ts @@ -20,7 +20,7 @@ function feature(geoid: string, name: string): Record { return { GEOID: geoid, NAME: name } } -function fakeResponse(opts: { ok: boolean; json: () => unknown | Promise }): Response { +function fakeResponse(opts: { ok: boolean; json: () => unknown }): Response { return { ok: opts.ok, json: async () => opts.json(), @@ -94,13 +94,14 @@ describe("parseCensusGeographies (pure)", () => { describe("CensusJurisdictionLookup.lookup (injected fake fetch)", () => { it("returns the parsed result on a 200 + valid body, and hits the injected base URL with the right query", async () => { let calledUrl = "" - const fetchImpl = (async (input: Parameters[0]) => { + const fetchImpl = async (input: Parameters[0]) => { + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- the adapter under test calls fetch with a string URL calledUrl = String(input) return fakeResponse({ ok: true, json: () => body({ "Incorporated Places": [feature("0644000", "Los Angeles")] }), }) - }) as unknown as typeof fetch + } const lookup = new CensusJurisdictionLookup({ baseUrl: BASE_URL, timeoutMs: 50, fetchImpl }) const result = await lookup.lookup(34.05, -118.25) @@ -142,7 +143,7 @@ describe("CensusJurisdictionLookup.lookup (injected fake fetch)", () => { }) it("THROWS unavailable when the request is aborted/times out", async () => { - const fetchImpl = ((_url: Parameters[0], init?: Parameters[1]) => + const fetchImpl = (_url: Parameters[0], init?: Parameters[1]) => new Promise((_resolve, reject) => { const signal = init?.signal if (signal) { @@ -150,7 +151,7 @@ describe("CensusJurisdictionLookup.lookup (injected fake fetch)", () => { reject(new DOMException("The operation was aborted.", "AbortError")) }) } - })) as unknown as typeof fetch + }) const lookup = new CensusJurisdictionLookup({ baseUrl: BASE_URL, timeoutMs: 1, fetchImpl }) await expect(lookup.lookup(1, 2)).rejects.toBeInstanceOf(JurisdictionLookupUnavailableError) }) diff --git a/services/api/test/unit/report-verification.test.ts b/services/api/test/unit/report-verification.test.ts index 94bcae0a..62846e57 100644 --- a/services/api/test/unit/report-verification.test.ts +++ b/services/api/test/unit/report-verification.test.ts @@ -295,7 +295,7 @@ describe("createReport auto-forward enqueue gate (D9)", () => { }) describe("report.autoforward handler (D9): runAutoForwardWith", () => { - function handlerHarness(opts: { sendError?: unknown } = {}) { + function handlerHarness({ sendError }: { sendError?: Error } = {}) { const repo = new InMemoryAdminReportRepository() repo.now = NOW const mailer = new FakeMailer() @@ -306,14 +306,14 @@ describe("report.autoforward handler (D9): runAutoForwardWith", () => { env: { MAIL_FROM_OUTREACH: "outreach@civfix.org", MAIL_REPLY_DOMAIN: "civfix.org" }, }) const outboundMail: OutboundMailService = - opts.sendError !== undefined + sendError !== undefined ? { ...realOutbound, async prepareReportToJurisdiction(input) { const prepared = await realOutbound.prepareReportToJurisdiction(input) - return { thread: prepared.thread, deliver: () => Promise.reject(opts.sendError) } + return { thread: prepared.thread, deliver: () => Promise.reject(sendError) } }, - sendReportToJurisdiction: () => Promise.reject(opts.sendError), + sendReportToJurisdiction: () => Promise.reject(sendError), } : realOutbound const svc = makeAdminReportService({ repo, outboundMail, now: () => NOW }) diff --git a/services/api/test/unit/sms-twilio.test.ts b/services/api/test/unit/sms-twilio.test.ts index 6a7a52f0..8dd9564c 100644 --- a/services/api/test/unit/sms-twilio.test.ts +++ b/services/api/test/unit/sms-twilio.test.ts @@ -55,6 +55,7 @@ describe("TwilioSmsSender", () => { expect(headers.authorization).toBe(`Basic ${expected}`) expect(headers["content-type"]).toBe("application/x-www-form-urlencoded") + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- the adapter posts a string form body const form = new URLSearchParams(String(call?.init.body)) expect(form.get("To")).toBe("+15552223333") expect(form.get("From")).toBe("+15550001111") diff --git a/services/media-worker/src/download.ts b/services/media-worker/src/download.ts index 718dc2fd..b4c496ea 100644 --- a/services/media-worker/src/download.ts +++ b/services/media-worker/src/download.ts @@ -24,6 +24,7 @@ export class DownloadTooLargeError extends Error { export class StorageUnavailableError extends Error { constructor(r2Key: string, cause?: unknown) { + // eslint-disable-next-line @typescript-eslint/no-base-to-string -- best-effort detail for a non-Error cause in an error message const detail = cause instanceof Error ? cause.message : cause !== undefined ? String(cause) : "" super(`storage unavailable for ${r2Key}${detail ? `: ${detail}` : ""}`) this.name = "StorageUnavailableError" @@ -160,5 +161,5 @@ async function proxyAwareFetch(url: string, signal: AbortSignal): Promise { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors -- this test is about a non-Error rejection reason await startWithHandler(() => Promise.reject("stringly-typed failure")) await lastBoss().deliver([{ id: "only", data: null }]) @@ -425,7 +426,7 @@ describe("PgBossWorkerJobs unsupported name-agnostic complete/fail", () => { describe("makeJobs seam selection", () => { it("defaults to the fake outside production (the worker boots offline)", () => { - const handle = makeJobs({ NODE_ENV: "test" } as NodeJS.ProcessEnv) + const handle = makeJobs({ NODE_ENV: "test" }) expect(handle.jobs).toBeInstanceOf(FakeJobs) }) @@ -439,12 +440,12 @@ describe("makeJobs seam selection", () => { NODE_ENV: "production", USE_FAKE_JOBS: "1", DATABASE_URL: "postgres://stub/civfix", - } as NodeJS.ProcessEnv), + }), ).toThrow(/USE_FAKE_JOBS must be 0 in production/) }) it("THROWS when the real seam is selected without a DATABASE_URL", () => { - expect(() => makeJobs({ NODE_ENV: "test", USE_FAKE_JOBS: "0" } as NodeJS.ProcessEnv)).toThrow( + expect(() => makeJobs({ NODE_ENV: "test", USE_FAKE_JOBS: "0" })).toThrow( /DATABASE_URL is required when USE_FAKE_JOBS is off/, ) }) @@ -455,7 +456,7 @@ describe("makeJobs seam selection", () => { DATABASE_URL: "postgres://stub/civfix", MEDIA_JOB_TIMEOUT_MS: "10000", MEDIA_IMAGE_TIMEOUT_MS: "1000", - } as NodeJS.ProcessEnv) + }) expect(handle.jobs).toBeInstanceOf(PgBossWorkerJobs) await handle.start() From 324bc6a0183951b98bacf429b96d5bf9f36a632c Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 23 Sep 2026 20:30:10 +0000 Subject: [PATCH 3/6] no unused locals --- services/api/src/services/data-export-service.ts | 8 +++++--- tsconfig.base.json | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/services/api/src/services/data-export-service.ts b/services/api/src/services/data-export-service.ts index e9ba1c68..3ec97d27 100644 --- a/services/api/src/services/data-export-service.ts +++ b/services/api/src/services/data-export-service.ts @@ -170,8 +170,10 @@ const DATA_EXPORT_FIT_ORDER = [ type UnfittedSection = Exclude -// A section missing from the fit order would silently drop out of every export. -const _everySectionIsFitted: UnfittedSection extends never ? true : never = true +// A section missing from the fit order would silently drop out of every export; a gap makes this +// binding's type `never`, so the assignment stops compiling. +const EXHAUSTIVE_FIT_ORDER: UnfittedSection extends never ? typeof DATA_EXPORT_FIT_ORDER : never = + DATA_EXPORT_FIT_ORDER interface SectionPart { kind: "section" @@ -373,7 +375,7 @@ function fitBody(loaded: ReadonlyMap): FittedBody { } const sections = new Map() - for (const name of DATA_EXPORT_FIT_ORDER) { + for (const name of EXHAUSTIVE_FIT_ORDER) { const { rowCap, redact, present } = DATA_EXPORT_BODY[name] const rows = loaded.get(name) ?? [] const kept = fit(name, redact ? rows.map(redact) : rows, rowCap) diff --git a/tsconfig.base.json b/tsconfig.base.json index 2625e0db..7644c72e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -8,6 +8,7 @@ "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, + "noUnusedLocals": true, "noFallthroughCasesInSwitch": true, "forceConsistentCasingInFileNames": true, "esModuleInterop": true, From cd185cb5dbb77c5de432bc5c02b3d2869dff3f54 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 23 Sep 2026 20:30:10 +0000 Subject: [PATCH 4/6] type-aware lint rules for the api and media worker --- packages/config/eslint.config.js | 31 ++++++++++++++++++++++++-- services/api/eslint.config.js | 11 +++++++-- services/media-worker/eslint.config.js | 4 ++-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/config/eslint.config.js b/packages/config/eslint.config.js index 67fe0262..fba59b64 100644 --- a/packages/config/eslint.config.js +++ b/packages/config/eslint.config.js @@ -2,8 +2,35 @@ import eslint from "@eslint/js" import tseslint from "typescript-eslint" import globals from "globals" -// Type-aware rules are enabled per service via `parserOptions.projectService` in the service's -// eslint.config.js, so this preset stays project-agnostic. +// Type-aware rules need a TypeScript program, which only the consuming service can locate, so each +// service passes its own parserOptions (projectService + tsconfigRootDir) through `typed()`. +export function typed(parserOptions) { + return { + files: ["**/*.ts"], + languageOptions: { parserOptions }, + rules: { + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/await-thenable": "error", + "@typescript-eslint/no-for-in-array": "error", + "@typescript-eslint/no-implied-eval": "error", + "@typescript-eslint/only-throw-error": "error", + "@typescript-eslint/prefer-promise-reject-errors": "error", + "@typescript-eslint/restrict-template-expressions": [ + "error", + { allowNumber: true, allowBoolean: true }, + ], + "@typescript-eslint/no-base-to-string": "error", + "@typescript-eslint/no-redundant-type-constituents": "error", + "@typescript-eslint/switch-exhaustiveness-check": [ + "error", + { considerDefaultExhaustiveForUnions: true }, + ], + "@typescript-eslint/no-unnecessary-type-assertion": "error", + }, + } +} + export function config(...extra) { return tseslint.config( { diff --git a/services/api/eslint.config.js b/services/api/eslint.config.js index 0254da4f..0ddd2fa0 100644 --- a/services/api/eslint.config.js +++ b/services/api/eslint.config.js @@ -1,3 +1,10 @@ -import { config } from "@civfix/config/eslint" +import { config, typed } from "@civfix/config/eslint" -export default config({}) +export default config( + typed({ + // scripts/ are tsx-run ops tools outside tsconfig.json's include; they lint against its + // compiler options instead of widening what `tsc --noEmit` checks. + projectService: { allowDefaultProject: ["scripts/*.ts"] }, + tsconfigRootDir: import.meta.dirname, + }), +) diff --git a/services/media-worker/eslint.config.js b/services/media-worker/eslint.config.js index 0c7727f7..7dd38dbd 100644 --- a/services/media-worker/eslint.config.js +++ b/services/media-worker/eslint.config.js @@ -1,3 +1,3 @@ -import { config } from "@civfix/config/eslint" +import { config, typed } from "@civfix/config/eslint" -export default config() +export default config(typed({ projectService: true, tsconfigRootDir: import.meta.dirname })) From 41f23da62e96ffb8806033fbb4acbc3f1c71cf65 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 23 Sep 2026 20:55:57 +0000 Subject: [PATCH 5/6] un-export what nothing imports; mark intended aliases --- services/api/src/adapters/email-layout.ts | 2 +- services/api/src/adapters/http-fetch.ts | 2 +- services/api/src/db/like.ts | 2 +- services/api/src/db/pg-errors.ts | 2 +- services/api/src/env.ts | 2 +- services/api/src/errors/pan-redaction.ts | 2 +- services/api/src/errors/sms-failure.ts | 2 +- services/api/src/lifecycle.ts | 1 + services/api/src/routes/map.routes.ts | 7 +------ services/api/src/routes/query-encoding.ts | 2 +- .../services/admin/admin-event-repository.drizzle.ts | 6 +++--- .../admin/jurisdiction-contacts-repository.drizzle.ts | 4 ++-- .../api/src/services/admin/mail-repository.drizzle.ts | 4 ++-- .../api/src/services/admin/outbound-send-policy.ts | 1 + services/api/src/services/chat-group-service.ts | 2 +- services/api/src/services/chat-reply-hydration.ts | 2 +- .../api/src/services/cleanup-map-repository.drizzle.ts | 2 +- .../api/src/services/cleanup-repository.drizzle.ts | 2 +- services/api/src/services/cleanup-sql.ts | 4 ++-- services/api/src/services/host/authz.ts | 2 +- .../host/broadcast-audience-repository.drizzle.ts | 10 ++-------- services/api/src/services/host/broadcast-audience.ts | 2 +- services/api/src/services/host/broadcast-render.ts | 8 ++------ services/api/src/services/host/broadcast-service.ts | 2 +- .../services/host/registration-repository.drizzle.ts | 1 - services/api/src/services/media-authorization.ts | 6 +++--- services/api/src/services/notification-service.ts | 2 -- services/api/src/services/post-repository.drizzle.ts | 2 +- services/api/src/ws/types.ts | 1 + services/api/test/helpers/cleanups.ts | 2 +- services/api/test/helpers/official-account.ts | 2 +- services/api/test/helpers/pg-container.ts | 2 +- services/api/test/helpers/ticket-signer.ts | 2 +- services/media-worker/src/jobs/retention-sweep.ts | 2 +- services/media-worker/test/fixtures/make.ts | 2 +- services/media-worker/test/helpers/pg.ts | 2 +- 36 files changed, 43 insertions(+), 58 deletions(-) diff --git a/services/api/src/adapters/email-layout.ts b/services/api/src/adapters/email-layout.ts index 7ece7d91..24dab818 100644 --- a/services/api/src/adapters/email-layout.ts +++ b/services/api/src/adapters/email-layout.ts @@ -36,7 +36,7 @@ const WORDMARK_HTML = WORDMARK.map( ([letter, color]) => `${letter}`, ).join("") -export const DEFAULT_FOOTER = +const DEFAULT_FOOTER = "civfix is a civic reporting platform that connects residents with their local government.\n" + "This mailbox is not monitored. civfix.org" diff --git a/services/api/src/adapters/http-fetch.ts b/services/api/src/adapters/http-fetch.ts index e48c824b..17d6cccc 100644 --- a/services/api/src/adapters/http-fetch.ts +++ b/services/api/src/adapters/http-fetch.ts @@ -6,7 +6,7 @@ export type FetchJsonResult = | { ok: false; kind: "transport"; error: unknown } | { ok: false; kind: "body"; status: number; error: unknown } -export const DEFAULT_MAX_JSON_BYTES = 1024 * 1024 +const DEFAULT_MAX_JSON_BYTES = 1024 * 1024 export class JsonBodyTooLargeError extends Error { constructor(public readonly maxBytes: number) { diff --git a/services/api/src/db/like.ts b/services/api/src/db/like.ts index e064db12..75cb973c 100644 --- a/services/api/src/db/like.ts +++ b/services/api/src/db/like.ts @@ -8,7 +8,7 @@ * backslash, but stating it keeps the query independent of that default. */ -export function escapeLike(term: string): string { +function escapeLike(term: string): string { return term.replace(/[\\%_]/g, (ch) => `\\${ch}`) } diff --git a/services/api/src/db/pg-errors.ts b/services/api/src/db/pg-errors.ts index e222e680..e43633a4 100644 --- a/services/api/src/db/pg-errors.ts +++ b/services/api/src/db/pg-errors.ts @@ -1,6 +1,6 @@ export const PG_UNIQUE_VIOLATION = "23505" -export const PG_CHECK_VIOLATION = "23514" +const PG_CHECK_VIOLATION = "23514" export const PG_SERIALIZATION_FAILURE = "40001" diff --git a/services/api/src/env.ts b/services/api/src/env.ts index d5166331..71567a6e 100644 --- a/services/api/src/env.ts +++ b/services/api/src/env.ts @@ -245,7 +245,7 @@ function loadedEnv(): Env { return cached } -export const env: Env = new Proxy({} as Env, { +const env: Env = new Proxy({} as Env, { get(_target, prop: string) { if (prop === "toJSON") return () => "[civfix env: redacted]" return loadedEnv()[prop as keyof Env] diff --git a/services/api/src/errors/pan-redaction.ts b/services/api/src/errors/pan-redaction.ts index 20ad2d56..094adf72 100644 --- a/services/api/src/errors/pan-redaction.ts +++ b/services/api/src/errors/pan-redaction.ts @@ -7,7 +7,7 @@ const CHAR_CODE_ZERO = 48 const LUHN_MODULUS = 10 const LUHN_MAX_DIGIT = 9 -export function isLuhnValid(candidate: string): boolean { +function isLuhnValid(candidate: string): boolean { const digits = candidate.replace(NON_DIGIT_RE, "") if (digits.length < PAN_MIN_DIGITS || digits.length > PAN_MAX_DIGITS) return false let sum = 0 diff --git a/services/api/src/errors/sms-failure.ts b/services/api/src/errors/sms-failure.ts index dc938a55..dabe0e90 100644 --- a/services/api/src/errors/sms-failure.ts +++ b/services/api/src/errors/sms-failure.ts @@ -1,6 +1,6 @@ import { AppError, ErrorCode } from "@civfix/shared" -export const SMS_FAILURE_FIELD = "smsDelivery" +const SMS_FAILURE_FIELD = "smsDelivery" const SMS_FAILURE_KINDS = ["opted_out", "invalid_number", "permanent", "temporary"] as const diff --git a/services/api/src/lifecycle.ts b/services/api/src/lifecycle.ts index b9a74b04..67b2d30d 100644 --- a/services/api/src/lifecycle.ts +++ b/services/api/src/lifecycle.ts @@ -11,6 +11,7 @@ declare module "fastify" { } export const REQUEST_TIMEOUT_MS = 15_000 +/** @alias */ export const SHUTDOWN_CLOSE_WAIT_MS = REQUEST_TIMEOUT_MS export const SHUTDOWN_TEARDOWN_WATCHDOG_MS = 15_000 const SHUTDOWN_IDLE_SWEEP_MS = 250 diff --git a/services/api/src/routes/map.routes.ts b/services/api/src/routes/map.routes.ts index 81aea04a..ddd311cd 100644 --- a/services/api/src/routes/map.routes.ts +++ b/services/api/src/routes/map.routes.ts @@ -22,17 +22,12 @@ import { makeCachedAddressResolver, makeRouteJurisdictionService, } from "../services/route-geo-helpers.js" -import { - makeDrizzleCleanupMapRepository, - MAP_CLEANUPS_LIMIT, -} from "../services/cleanup-map-repository.drizzle.js" +import { makeDrizzleCleanupMapRepository } from "../services/cleanup-map-repository.drizzle.js" import { insertAuditRow } from "../services/admin/audit-repository.drizzle.js" import { CappedBBoxQueryParam } from "./query-encoding.js" import { parse, trimTextFields } from "./_validate.js" import { route } from "../versioning/route.js" -export { MAP_CLEANUPS_LIMIT } - const CARTO_VOYAGER_RASTER_URL = "https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png" const CARTO_ATTRIBUTION = "(c) OpenStreetMap contributors, (c) CARTO" diff --git a/services/api/src/routes/query-encoding.ts b/services/api/src/routes/query-encoding.ts index c148a4b1..424c1fdb 100644 --- a/services/api/src/routes/query-encoding.ts +++ b/services/api/src/routes/query-encoding.ts @@ -57,7 +57,7 @@ const BBoxQueryParam = jsonParam(BBoxSchema).refine((b) => b.west < b.east && b. * scan up to that cap, and jittered bounds defeat caching. The globe is 64,800 deg^2; this admits a * continental view (a real, if rare, client state) and rejects only the pathological world scan. */ -export const MAX_MAP_BBOX_AREA_DEG2 = 40_000 +const MAX_MAP_BBOX_AREA_DEG2 = 40_000 /** * Every anonymous, bbox-driven map read must use this shape; it lives next to the ordering refine so the diff --git a/services/api/src/services/admin/admin-event-repository.drizzle.ts b/services/api/src/services/admin/admin-event-repository.drizzle.ts index 9ae9eed6..91997726 100644 --- a/services/api/src/services/admin/admin-event-repository.drizzle.ts +++ b/services/api/src/services/admin/admin-event-repository.drizzle.ts @@ -48,7 +48,7 @@ export function flaggedEventExpr(sql: Queryable): SqlFragment { } // Assumes the query selects `cleanups c` LEFT JOIN `users u`. -export function searchEventsFragment(sql: Queryable, q: string | null): SqlFragment { +function searchEventsFragment(sql: Queryable, q: string | null): SqlFragment { if (q === null) return sql`` // ilikeAnyOf escapes the LIKE metacharacters so %/_ in q match literally (wildcard injection/trigram DoS). return sql`AND ${ilikeAnyOf( @@ -83,7 +83,7 @@ export interface EventRowSelect { organizer_joined: Date | null } -export function toRecord(r: EventRowSelect): AdminEventRecord { +function toRecord(r: EventRowSelect): AdminEventRecord { const organizer: AdminOrganizerRecord | null = toPersonRecord( { id: r.organizer_id, @@ -115,7 +115,7 @@ export function toRecord(r: EventRowSelect): AdminEventRecord { } // Cleanups carry only a free-text address and no jurisdiction, so the place label is that address. -export function eventSelect( +function eventSelect( sql: Queryable, extraWhere: SqlFragment, orderLimit: SqlFragment, diff --git a/services/api/src/services/admin/jurisdiction-contacts-repository.drizzle.ts b/services/api/src/services/admin/jurisdiction-contacts-repository.drizzle.ts index f3b220a9..80c65581 100644 --- a/services/api/src/services/admin/jurisdiction-contacts-repository.drizzle.ts +++ b/services/api/src/services/admin/jurisdiction-contacts-repository.drizzle.ts @@ -552,7 +552,7 @@ export interface DirectoryFacetAggregate { // upsertJurisdictionContacts so every in-process writer of routing contacts can drop it after commit. let directoryFacetCache: { at: number; value: DirectoryFacetAggregate } | null = null -export function readDirectoryFacetCache(): DirectoryFacetAggregate | null { +function readDirectoryFacetCache(): DirectoryFacetAggregate | null { if (directoryFacetCache === null) return null if (Date.now() - directoryFacetCache.at > DIRECTORY_FACET_TTL_MS) { directoryFacetCache = null @@ -561,7 +561,7 @@ export function readDirectoryFacetCache(): DirectoryFacetAggregate | null { return directoryFacetCache.value } -export function writeDirectoryFacetCache(value: DirectoryFacetAggregate): void { +function writeDirectoryFacetCache(value: DirectoryFacetAggregate): void { directoryFacetCache = { at: Date.now(), value } } diff --git a/services/api/src/services/admin/mail-repository.drizzle.ts b/services/api/src/services/admin/mail-repository.drizzle.ts index ff3270c6..c5c64f4b 100644 --- a/services/api/src/services/admin/mail-repository.drizzle.ts +++ b/services/api/src/services/admin/mail-repository.drizzle.ts @@ -200,7 +200,7 @@ function bounceEventMatch(sql: Queryable, input: BounceEventKey): SqlFragment { ` } -export function latestOutboundAttempt(sql: Queryable, threadRef: SqlFragment): SqlFragment { +function latestOutboundAttempt(sql: Queryable, threadRef: SqlFragment): SqlFragment { return sql` SELECT m.id, m.created_at FROM mail_messages m @@ -210,7 +210,7 @@ export function latestOutboundAttempt(sql: Queryable, threadRef: SqlFragment): S ` } -export function attemptEventExists( +function attemptEventExists( sql: Queryable, threadRef: SqlFragment, type: "sent" | "failed", diff --git a/services/api/src/services/admin/outbound-send-policy.ts b/services/api/src/services/admin/outbound-send-policy.ts index 83184171..72be9438 100644 --- a/services/api/src/services/admin/outbound-send-policy.ts +++ b/services/api/src/services/admin/outbound-send-policy.ts @@ -76,6 +76,7 @@ export const ROUTE_DEADLINE_INFLIGHT_SECONDS = Math.max( }), ) +/** @alias */ export const ROUTE_CLAIM_STALE_SECONDS = ROUTE_DEADLINE_INFLIGHT_SECONDS export function assertOutboundSendPolicy(input: { diff --git a/services/api/src/services/chat-group-service.ts b/services/api/src/services/chat-group-service.ts index 7b98f43a..3ab4dff2 100644 --- a/services/api/src/services/chat-group-service.ts +++ b/services/api/src/services/chat-group-service.ts @@ -19,7 +19,7 @@ import { isOfficialAccount } from "../auth/official-account.js" import { clampPageLimit } from "../lib/page-limit.js" import { NO_AFFILIATIONS, withAffiliation, type AffiliationLoader } from "./affiliation.js" -export const GROUP_MEMBERS_DEFAULT_LIMIT = 25 +const GROUP_MEMBERS_DEFAULT_LIMIT = 25 const GROUP_MEMBERS_MAX_LIMIT = 50 const INVITE_BLOCK_SCAN_MEMBERS = 200 diff --git a/services/api/src/services/chat-reply-hydration.ts b/services/api/src/services/chat-reply-hydration.ts index 15674137..ca1937db 100644 --- a/services/api/src/services/chat-reply-hydration.ts +++ b/services/api/src/services/chat-reply-hydration.ts @@ -74,7 +74,7 @@ export async function assertReplyTarget( } /** Keyed by TARGET id. */ -export async function loadReplyTargets( +async function loadReplyTargets( sql: Queryable, table: ReplyTable, replyToIds: ReadonlyArray, diff --git a/services/api/src/services/cleanup-map-repository.drizzle.ts b/services/api/src/services/cleanup-map-repository.drizzle.ts index d8229f45..a70a31fa 100644 --- a/services/api/src/services/cleanup-map-repository.drizzle.ts +++ b/services/api/src/services/cleanup-map-repository.drizzle.ts @@ -8,7 +8,7 @@ import { } from "./cleanup-sql.js" import type { CleanupMapRepository } from "./cleanup-map-repository.js" -export const MAP_CLEANUPS_LIMIT = 500 +const MAP_CLEANUPS_LIMIT = 500 export function makeDrizzleCleanupMapRepository(sql: Sql): CleanupMapRepository { return { diff --git a/services/api/src/services/cleanup-repository.drizzle.ts b/services/api/src/services/cleanup-repository.drizzle.ts index 8b84b4b3..3ec4428a 100644 --- a/services/api/src/services/cleanup-repository.drizzle.ts +++ b/services/api/src/services/cleanup-repository.drizzle.ts @@ -1516,7 +1516,7 @@ export async function ensureSignupRegistrationIn( return registrationId } -export async function cancelSignupRegistrationIn( +async function cancelSignupRegistrationIn( tx: Queryable, args: { cleanupId: string; userId: string; actorId: string | null; now: Date }, ): Promise { diff --git a/services/api/src/services/cleanup-sql.ts b/services/api/src/services/cleanup-sql.ts index ff559c6b..3df2aea9 100644 --- a/services/api/src/services/cleanup-sql.ts +++ b/services/api/src/services/cleanup-sql.ts @@ -214,11 +214,11 @@ export function cleanupColumns(sql: Queryable, near: NearPoint | null) { ` } -export function memberCountScalar(sql: Queryable) { +function memberCountScalar(sql: Queryable) { return sql`(SELECT count(*)::int FROM cleanup_members m WHERE m.cleanup_id = c.id)` } -export function activeGuestCountScalar(sql: Queryable) { +function activeGuestCountScalar(sql: Queryable) { return sql`( SELECT count(*)::int FROM cleanup_guests cg WHERE cg.cleanup_id = c.id AND cg.cancelled_at IS NULL diff --git a/services/api/src/services/host/authz.ts b/services/api/src/services/host/authz.ts index 72fa77f6..bff5f75d 100644 --- a/services/api/src/services/host/authz.ts +++ b/services/api/src/services/host/authz.ts @@ -51,7 +51,7 @@ export function hasHostStanding(standing: HostStanding): boolean { return standing.eventRole !== null || standing.orgRole !== null } -export function notFoundCleanup(): AppError { +function notFoundCleanup(): AppError { return AppError.notFound("Cleanup not found") } diff --git a/services/api/src/services/host/broadcast-audience-repository.drizzle.ts b/services/api/src/services/host/broadcast-audience-repository.drizzle.ts index f3cd7900..d61514a2 100644 --- a/services/api/src/services/host/broadcast-audience-repository.drizzle.ts +++ b/services/api/src/services/host/broadcast-audience-repository.drizzle.ts @@ -26,19 +26,13 @@ interface CountRow { n: number } -export async function listMemberAudiencePage( - sql: Queryable, - query: AudienceQuery, -): Promise { +async function listMemberAudiencePage(sql: Queryable, query: AudienceQuery): Promise { const page = memberQuery(sql, query) const rows = page === null ? [] : await page return rows.map((row) => row.id) } -export async function listGuestAudiencePage( - sql: Queryable, - query: AudienceQuery, -): Promise { +async function listGuestAudiencePage(sql: Queryable, query: AudienceQuery): Promise { const page = guestQuery(sql, query) const rows = page === null ? [] : await page return rows.map((row) => row.id) diff --git a/services/api/src/services/host/broadcast-audience.ts b/services/api/src/services/host/broadcast-audience.ts index 852bcd63..0bd9c8e4 100644 --- a/services/api/src/services/host/broadcast-audience.ts +++ b/services/api/src/services/host/broadcast-audience.ts @@ -1,7 +1,7 @@ import type { BroadcastKind, BroadcastSegment } from "@civfix/shared" import type { BroadcastRepository } from "./broadcast-repository.js" -export const AUDIENCE_PAGE_SIZE = 1000 +const AUDIENCE_PAGE_SIZE = 1000 const AUDIENCE_MAX_PAGES = 100 // Sorts after every real id, so a side that is already exhausted pages to an empty result. const LAST_UUID = "ffffffff-ffff-ffff-ffff-ffffffffffff" diff --git a/services/api/src/services/host/broadcast-render.ts b/services/api/src/services/host/broadcast-render.ts index 1fb0df41..6502a743 100644 --- a/services/api/src/services/host/broadcast-render.ts +++ b/services/api/src/services/host/broadcast-render.ts @@ -111,17 +111,13 @@ export function formatEventWhen(scheduledAt: Date, timezone: string | null): str } } -export function eventPath(pageSlug: string | null, cleanupId: string): string { +function eventPath(pageSlug: string | null, cleanupId: string): string { return pageSlug !== null && pageSlug.length > 0 ? `/e/${encodeURIComponent(pageSlug)}` : `/cleanups/${cleanupId}` } -export function eventManageUrl( - webBaseUrl: string, - pageSlug: string | null, - cleanupId: string, -): string { +function eventManageUrl(webBaseUrl: string, pageSlug: string | null, cleanupId: string): string { return `${webBaseUrl}${eventPath(pageSlug, cleanupId)}` } diff --git a/services/api/src/services/host/broadcast-service.ts b/services/api/src/services/host/broadcast-service.ts index cdb3c6a6..0364ff96 100644 --- a/services/api/src/services/host/broadcast-service.ts +++ b/services/api/src/services/host/broadcast-service.ts @@ -121,7 +121,7 @@ export function emailHashOf(email: string): string { return sha256HexSync(email.trim().toLowerCase()) } -export function utcDayKey(at: Date): string { +function utcDayKey(at: Date): string { return at.toISOString().slice(0, ISO_DAY_LENGTH) } diff --git a/services/api/src/services/host/registration-repository.drizzle.ts b/services/api/src/services/host/registration-repository.drizzle.ts index 955bbc1c..0e3dc299 100644 --- a/services/api/src/services/host/registration-repository.drizzle.ts +++ b/services/api/src/services/host/registration-repository.drizzle.ts @@ -14,7 +14,6 @@ import type { export { REGISTER_IDEMPOTENCY_SCOPE } from "./registration-repository-register.drizzle.js" export { applyBanIn } from "./registration-repository-roster.drizzle.js" -export { cancelWaitlistEntriesIn } from "./registration-repository-waitlist.drizzle.js" export function makeDrizzleHostRegistrationRepository(sql: Sql): HostRegistrationRepository { return { diff --git a/services/api/src/services/media-authorization.ts b/services/api/src/services/media-authorization.ts index e8740007..38e3fd82 100644 --- a/services/api/src/services/media-authorization.ts +++ b/services/api/src/services/media-authorization.ts @@ -74,7 +74,7 @@ export function makeDrizzleMediaViewAuthorizer( } } -export async function authorizeChatBound( +async function authorizeChatBound( repo: MediaAuthorizationRepository, messageId: string, viewerId: string | null, @@ -124,7 +124,7 @@ async function authorizeGroupRoom( return group.isMember || group.visibility === "public" ? ALLOW_PRIVATE : DENY } -export async function authorizeEventBound( +async function authorizeEventBound( repo: MediaAuthorizationRepository, asset: MediaAssetView, viewerId: string | null, @@ -136,7 +136,7 @@ export async function authorizeEventBound( return event.isMember ? ALLOW_PRIVATE : DENY } -export async function authorizeOrgLogoBound( +async function authorizeOrgLogoBound( repo: MediaAuthorizationRepository, asset: MediaAssetView, now: Date, diff --git a/services/api/src/services/notification-service.ts b/services/api/src/services/notification-service.ts index 966b79ae..0cf93f10 100644 --- a/services/api/src/services/notification-service.ts +++ b/services/api/src/services/notification-service.ts @@ -39,8 +39,6 @@ export { isFeedVisibleType, isWithinQuietHours, parseTimeOfDayMinutes, - pushGateAllows, - toNotificationDTO, toPrefsDTO, typeAllowedByPrefs, } from "./notification-helpers.js" diff --git a/services/api/src/services/post-repository.drizzle.ts b/services/api/src/services/post-repository.drizzle.ts index 125ff207..6ca8fb23 100644 --- a/services/api/src/services/post-repository.drizzle.ts +++ b/services/api/src/services/post-repository.drizzle.ts @@ -376,7 +376,7 @@ function viewerBlockClause(sql: Queryable, viewerId: string): SqlFragment { )` } -export function feedCandidatesStatement(sql: Queryable, args: FeedCandidateArgs): SqlFragment { +function feedCandidatesStatement(sql: Queryable, args: FeedCandidateArgs): SqlFragment { const viewerId = args.viewerId const filterClause = feedFilterClause(sql, args.filter) const eligible = (): SqlFragment => sql` diff --git a/services/api/src/ws/types.ts b/services/api/src/ws/types.ts index 3de367d0..cdff1594 100644 --- a/services/api/src/ws/types.ts +++ b/services/api/src/ws/types.ts @@ -44,6 +44,7 @@ export const WS_MAX_QUEUED_FRAMES = Math.max(WS_FRAME_LIMIT.capacity, WS_MAX_JOI // A reconnecting client pipelines the same burst before its handshake settles. A frame over this cap is // dropped without any reply, so it must not be smaller than what the live socket would queue and answer. +/** @alias */ export const WS_HANDSHAKE_FRAME_BUFFER = WS_MAX_QUEUED_FRAMES export const WS_HANDSHAKE_BUFFER_BYTES = 64 * 1024 diff --git a/services/api/test/helpers/cleanups.ts b/services/api/test/helpers/cleanups.ts index 5e46b9a6..01b61823 100644 --- a/services/api/test/helpers/cleanups.ts +++ b/services/api/test/helpers/cleanups.ts @@ -267,7 +267,7 @@ interface StoredSlotClaim { slotId: string } -export function haversineMeters(a: NearPoint, b: NearPoint): number { +function haversineMeters(a: NearPoint, b: NearPoint): number { const R = 6371008.8 const toRad = (d: number): number => (d * Math.PI) / 180 const dLat = toRad(b.lat - a.lat) diff --git a/services/api/test/helpers/official-account.ts b/services/api/test/helpers/official-account.ts index 3f8d3921..1bd56516 100644 --- a/services/api/test/helpers/official-account.ts +++ b/services/api/test/helpers/official-account.ts @@ -3,7 +3,7 @@ import { dirname, join } from "node:path" import { fileURLToPath } from "node:url" import type { Queryable } from "../../src/db/client.js" -export const OFFICIAL_ACCOUNT_MIGRATION = join( +const OFFICIAL_ACCOUNT_MIGRATION = join( dirname(fileURLToPath(import.meta.url)), "..", "..", diff --git a/services/api/test/helpers/pg-container.ts b/services/api/test/helpers/pg-container.ts index dd627b39..77bccb18 100644 --- a/services/api/test/helpers/pg-container.ts +++ b/services/api/test/helpers/pg-container.ts @@ -24,7 +24,7 @@ import { applyMigrations } from "../../src/db/migrate.js" import { seedJurisdictions } from "../../src/db/seed.js" /** The PostGIS image we run. Pinned so spatial behavior is reproducible. */ -export const POSTGIS_IMAGE = "postgis/postgis:16-3.4" +const POSTGIS_IMAGE = "postgis/postgis:16-3.4" /** * The migrated+seeded database every test database is cloned from. Never connected to by a test: a diff --git a/services/api/test/helpers/ticket-signer.ts b/services/api/test/helpers/ticket-signer.ts index 89f7fdf8..1c4f30f0 100644 --- a/services/api/test/helpers/ticket-signer.ts +++ b/services/api/test/helpers/ticket-signer.ts @@ -1,5 +1,5 @@ import { makeTicketTokenSigner } from "../../src/services/host/ticket-token.js" -export const TEST_TICKET_TOKEN_SECRET = "test-ticket-token-secret-32-chars-min" +const TEST_TICKET_TOKEN_SECRET = "test-ticket-token-secret-32-chars-min" export const TEST_TICKET_SIGNER = makeTicketTokenSigner(TEST_TICKET_TOKEN_SECRET) diff --git a/services/media-worker/src/jobs/retention-sweep.ts b/services/media-worker/src/jobs/retention-sweep.ts index 9328b78c..b67c5a7f 100644 --- a/services/media-worker/src/jobs/retention-sweep.ts +++ b/services/media-worker/src/jobs/retention-sweep.ts @@ -43,7 +43,7 @@ const RETENTION_IDEMPOTENCY_MS = 48 * MS_PER_HOUR const RETENTION_NOTIFICATIONS_MS = 90 * MS_PER_DAY export const RETENTION_GEOCODE_CACHE_MS = GEOCODE_CACHE_TTL_MS export const RETENTION_INBOUND_EMAILS_MS = INBOUND_EMAIL_RETENTION_MS -export const RETENTION_MAX_PAGES = 20 +const RETENTION_MAX_PAGES = 20 export async function runRetentionSweep(deps: RetentionSweepDeps): Promise { const { log, report, now: clock } = resolveJobObs(deps) diff --git a/services/media-worker/test/fixtures/make.ts b/services/media-worker/test/fixtures/make.ts index 47270abc..54792938 100644 --- a/services/media-worker/test/fixtures/make.ts +++ b/services/media-worker/test/fixtures/make.ts @@ -5,7 +5,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { join } from "node:path" -export function buildExifGpsApp1(lat: number, lng: number): Buffer { +function buildExifGpsApp1(lat: number, lng: number): Buffer { const toRat = (deg: number): [number, number][] => { const d = Math.floor(deg) const mf = (deg - d) * 60 diff --git a/services/media-worker/test/helpers/pg.ts b/services/media-worker/test/helpers/pg.ts index 468f3ac4..6ce5b395 100644 --- a/services/media-worker/test/helpers/pg.ts +++ b/services/media-worker/test/helpers/pg.ts @@ -29,7 +29,7 @@ import { drizzle } from "drizzle-orm/postgres-js" import { schema, type Db, type Sql } from "@civfix/api/db" import { applyMigrations } from "@civfix/api/migrate" -export const POSTGIS_IMAGE = "postgis/postgis:16-3.4" +const POSTGIS_IMAGE = "postgis/postgis:16-3.4" /** * Server knobs for a THROWAWAY database, matching the API harness's POSTGRES_TUNING: durability is From 93cd70f045e4f59c59ea02892e7460301894b08c Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 23 Sep 2026 20:55:57 +0000 Subject: [PATCH 6/6] ci checks formatting, dynamic sql, unused code, duplication and the email worker --- .github/workflows/ci.yml | 20 + .jscpd.json | 14 + .prettierignore | 2 + CONTRIBUTING.md | 7 +- README.md | 15 +- knip.jsonc | 52 +++ package.json | 5 + pnpm-lock.yaml | 600 ++++++++++++++++++++++++++--- services/api/package.json | 1 - services/media-worker/package.json | 1 - 10 files changed, 663 insertions(+), 54 deletions(-) create mode 100644 .jscpd.json create mode 100644 knip.jsonc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d02a2a32..002c3da6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,29 @@ jobs: - name: Lint run: pnpm lint + - name: Format check + run: pnpm format:check + - name: Typecheck run: pnpm typecheck + - name: Dynamic-SQL guard + run: pnpm check:sql + + # The email worker is not a workspace package: it deploys with wrangler from its own lockfile, so + # it installs in isolation. knip below analyzes it too, so this install comes first. + - name: Email worker install + run: pnpm --dir infra/email-worker install --frozen-lockfile --ignore-workspace + + - name: Email worker test + run: pnpm --dir infra/email-worker test + + - name: Unused files, exports and dependencies (knip) + run: pnpm knip + + - name: Duplication (jscpd) + run: pnpm dup:check + - name: Build run: pnpm build diff --git a/.jscpd.json b/.jscpd.json new file mode 100644 index 00000000..5a01075b --- /dev/null +++ b/.jscpd.json @@ -0,0 +1,14 @@ +{ + "path": ["services/api/src", "services/media-worker/src", "packages"], + "format": ["typescript"], + "ignore": [ + "**/*.test.ts", + "**/*.generated.ts", + "**/drizzle/**", + "**/node_modules/**", + "**/dist/**" + ], + "minTokens": 70, + "reporters": ["console"], + "threshold": 0.5 +} diff --git a/.prettierignore b/.prettierignore index 1c44534b..7a43fb71 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,5 @@ coverage shared drizzle pnpm-lock.yaml +# Prose and tables are hand-laid-out and not machine-formatted; the formatting pass covered code only. +*.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99e640bb..f3b908e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,5 +49,8 @@ a pull request that adds one. - Validate every input at the boundary, check authorization on every path, keep secrets out of code and logs, minimize personal data. - No N+1 queries, index new query paths, bound every result set. -- Add or update tests with the change. Typecheck, lint and tests pass in the - repository before the PR opens. +- Add or update tests with the change. Before the PR opens, these pass from + the repository root: `pnpm lint`, `pnpm typecheck`, `pnpm format:check`, + `pnpm check:sql`, `pnpm knip`, `pnpm dup:check` and `pnpm test`, plus the + email worker's own tests (`pnpm --dir infra/email-worker test`) when it + changes. CI runs the same checks. diff --git a/README.md b/README.md index 075aed56..89a3c295 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ civfix-backend/ caddy/ reverse-proxy Caddyfile secrets/ SOPS + age docs and .sops.yaml tiles/ note: map uses OpenStreetMap (CARTO Voyager) raster; no self-hosted tiles - .github/workflows/ci.yml lint / typecheck / build / test + integration services + .github/workflows/ci.yml lint / format / typecheck / static checks / build / test tsconfig.base.json strict base TS config turbo.json turborepo task graph pnpm-workspace.yaml @@ -84,10 +84,23 @@ pnpm build # build @civfix/shared, then api + media-worker pnpm typecheck # tsc --noEmit across all packages pnpm lint # eslint across all packages pnpm test # vitest unit tests +pnpm format:check # prettier --check (Markdown is excluded, see .prettierignore) +pnpm check:sql # dynamic-SQL guard: unsafe/raw allowlist, Drizzle $client, parameter IS NULL tests +pnpm knip # unused files, exports and dependencies (knip.jsonc) +pnpm dup:check # copy-paste duplication over the service sources (.jscpd.json threshold) pnpm dev # run services in watch mode (persistent) pnpm clean # remove build artifacts ``` +CI runs every command above except `dev` and `clean`. The Cloudflare email worker in +`infra/email-worker` is not a workspace package and keeps its own lockfile, so it installs and tests +in isolation: + +``` +pnpm --dir infra/email-worker install --frozen-lockfile --ignore-workspace +pnpm --dir infra/email-worker test +``` + ## Running the API in dev (offline, no credentials) Outside production the env loader supplies insecure dev defaults for the signing keys and turns on diff --git a/knip.jsonc b/knip.jsonc new file mode 100644 index 00000000..45c7aa97 --- /dev/null +++ b/knip.jsonc @@ -0,0 +1,52 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema-jsonc.json", + // A type that appears in an exported signature stays exported so callers can name it. + "ignoreExportsUsedInFile": { "interface": true, "type": true }, + "workspaces": { + ".": { + // check-dynamic-sql.d.mts types the .mjs guard for the api tests that import its matchers. + "entry": ["scripts/*.mjs", "scripts/check-dynamic-sql.d.mts"], + "project": ["scripts/**"], + }, + // The package.json scripts, the tsup entries, the vitest globalSetup and drizzle.config.ts are entries + // through knip's plugins; listed here are only the hand-run CLIs nothing else names. + "services/api": { + "entry": [ + // The font-change procedure in assets/fonts/PROVENANCE.md runs it with tsx. + "scripts/render-sample-certificate.ts", + // The only writer of jurisdictions.handle; whether to wire or retire it is an open decision. + "src/db/backfill-jurisdiction-handles.ts", + ], + "project": ["src/**/*.ts", "scripts/**/*.ts", "test/**/*.ts"], + "ignore": [ + // Hand-run SQL transcript tooling: the generator is a tsx CLI, and the generated cases reach + // every exported SQL helper through a namespace import that static analysis cannot follow. + "test/sql-transcripts/**", + // Unused since it landed; deleting it waits on the roster-export vs host-contact consent + // decision, whose fix may reuse hostContactOptInFor. + "src/services/host/event-consents-repository.drizzle.ts", + ], + }, + "services/media-worker": { + "project": ["src/**/*.ts", "test/**/*.ts"], + // tsup inlines @civfix/api (noExternal) but keeps these external, so the bundled api code + // resolves them from the worker's own node_modules at runtime. + "ignoreDependencies": [ + // @civfix/api/adapters/storage (R2 client) + "@aws-sdk/client-s3", + // @civfix/api/adapters/storage (presigned URLs) + "@aws-sdk/s3-request-presigner", + // @civfix/api/errors (GlitchTip reporting) + "@sentry/node", + // @civfix/api/adapters/storage (R2 request timeouts and egress proxy) + "@smithy/node-http-handler", + // @civfix/api/adapters/storage (egress proxy agent) + "https-proxy-agent", + ], + }, + "packages/config": {}, + // Outside pnpm-workspace.yaml (it deploys with wrangler from its own lockfile); CI installs it before + // knip runs so its dependencies resolve. + "infra/email-worker": {}, + }, +} diff --git a/package.json b/package.json index 07894d04..7719e74f 100644 --- a/package.json +++ b/package.json @@ -46,10 +46,15 @@ "lint": "turbo run lint", "test": "turbo run test --continue", "check:sql": "node scripts/check-dynamic-sql.mjs", + "format:check": "prettier --check .", + "knip": "knip", + "dup:check": "jscpd", "dev": "turbo run dev", "clean": "turbo run clean" }, "devDependencies": { + "jscpd": "^5.3.2", + "knip": "^5.88.1", "prettier": "^3.4.2", "turbo": "^2.3.3", "typescript": "^5.6.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 332e448d..8c733794 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,12 @@ importers: .: devDependencies: + jscpd: + specifier: ^5.3.2 + version: 5.3.2 + knip: + specifier: ^5.88.1 + version: 5.88.1(@types/node@22.20.3)(typescript@5.9.3) prettier: specifier: ^3.4.2 version: 3.8.3 @@ -48,13 +54,13 @@ importers: version: 9.39.4 eslint: specifier: ^9.16.0 - version: 9.39.4 + version: 9.39.4(jiti@2.7.0) globals: specifier: ^15.13.0 version: 15.15.0 typescript-eslint: specifier: ^8.18.0 - version: 8.60.0(eslint@9.39.4)(typescript@5.9.3) + version: 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) services/api: dependencies: @@ -178,19 +184,16 @@ importers: version: 0.31.10 eslint: specifier: ^9.16.0 - version: 9.39.4 + version: 9.39.4(jiti@2.7.0) ioredis-mock: specifier: ^8.9.0 version: 8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.11.0))(ioredis@5.11.0) rimraf: specifier: ^6.0.1 version: 6.1.3 - testcontainers: - specifier: ^11.14.0 - version: 11.14.0 tsup: specifier: ^8.3.5 - version: 8.5.1(postcss@8.5.28)(tsx@4.23.13)(typescript@5.9.3)(yaml@2.9.0) + version: 8.5.1(jiti@2.7.0)(postcss@8.5.28)(tsx@4.23.13)(typescript@5.9.3)(yaml@2.9.0) tsx: specifier: ^4.23.13 version: 4.23.13 @@ -199,7 +202,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.7 - version: 3.2.7(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0) + version: 3.2.7(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) services/media-worker: dependencies: @@ -263,7 +266,7 @@ importers: version: 22.19.19 eslint: specifier: ^9.16.0 - version: 9.39.4 + version: 9.39.4(jiti@2.7.0) ffmpeg-static: specifier: ^5.2.0 version: 5.3.0 @@ -273,12 +276,9 @@ importers: rimraf: specifier: ^6.0.1 version: 6.1.3 - testcontainers: - specifier: ^11.14.0 - version: 11.14.0 tsup: specifier: ^8.3.5 - version: 8.5.1(postcss@8.5.28)(tsx@4.23.13)(typescript@5.9.3)(yaml@2.9.0) + version: 8.5.1(jiti@2.7.0)(postcss@8.5.28)(tsx@4.23.13)(typescript@5.9.3)(yaml@2.9.0) tsx: specifier: ^4.23.13 version: 4.23.13 @@ -287,7 +287,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.7 - version: 3.2.7(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0) + version: 3.2.7(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) packages: @@ -434,18 +434,27 @@ packages: '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + '@emnapi/runtime@0.45.0': resolution: {integrity: sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==} '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.hirok.io' @@ -1108,6 +1117,13 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -1380,6 +1396,18 @@ packages: resolution: {integrity: sha512-u2OlIxW264bFUfvbFqDz9HZKFjwe8FHFtn7T/U8mYjPZ7DWYpbUB+/dkW/QgYfMSfR0ejkyuWaBBe0coW7/7ig==} engines: {node: '>= 10'} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.220.0': resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} engines: {node: '>=8.0.0'} @@ -1424,12 +1452,15 @@ packages: '@oslojs/asn1@1.0.0': resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oslojs/binary@1.0.0': resolution: {integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oslojs/crypto@1.0.1': resolution: {integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@oslojs/encoding@0.4.1': resolution: {integrity: sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==} @@ -1439,6 +1470,102 @@ packages: '@oslojs/jwt@0.2.0': resolution: {integrity: sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + resolution: {integrity: sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.24.2': + resolution: {integrity: sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + resolution: {integrity: sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.24.2': + resolution: {integrity: sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + resolution: {integrity: sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + resolution: {integrity: sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + resolution: {integrity: sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + resolution: {integrity: sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + resolution: {integrity: sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + resolution: {integrity: sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + resolution: {integrity: sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + resolution: {integrity: sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + resolution: {integrity: sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + resolution: {integrity: sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + resolution: {integrity: sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + resolution: {integrity: sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + resolution: {integrity: sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + resolution: {integrity: sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + resolution: {integrity: sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==} + cpu: [x64] + os: [win32] '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -1735,6 +1862,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + '@tybys/wasm-util@0.8.3': resolution: {integrity: sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==} @@ -1974,6 +2104,7 @@ packages: arctic@2.3.4: resolution: {integrity: sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2103,6 +2234,10 @@ packages: resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + brotli@1.3.3: resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} @@ -2247,6 +2382,7 @@ packages: cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -2521,6 +2657,7 @@ packages: eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -2589,6 +2726,10 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2631,6 +2772,9 @@ packages: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} engines: {node: '>=0.8.0'} + 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'} @@ -2667,6 +2811,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + find-my-way@9.9.0: resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==} engines: {node: '>=20'} @@ -2700,6 +2848,11 @@ packages: resolution: {integrity: sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==} engines: {node: '>= 0.12'} + 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'} @@ -2760,6 +2913,10 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -2954,6 +3111,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -2979,6 +3140,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + 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==} @@ -2999,6 +3164,51 @@ packages: resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true + jscpd-darwin-arm64@5.3.2: + resolution: {integrity: sha512-dlNh+revPs3X1yTRNAYqZD9u7eqiR9I+KoNvEk9JzSwzOuaUQrgwpP5EZQB0NKuF0oZZZeKrk05OTsNorVpqmQ==} + cpu: [arm64] + os: [darwin] + + jscpd-darwin-x64@5.3.2: + resolution: {integrity: sha512-JUt9dhLw2k1WNLqbFcqbDf8ErR6mp9ucKn+chRBsBRdx7MNbdEXmV5UuHRBO8d9h7Cw1eSpfpBPMTCXOS4aaeA==} + cpu: [x64] + os: [darwin] + + jscpd-linux-arm64-gnu@5.3.2: + resolution: {integrity: sha512-4+Z/fW7nqruf47AXRXPe8nTeExPm93Gt4A/6B4Wsi6b0G8Ei/tYDxB4jAwUBXnOGVwxrr/IDJlgSEvHLbzX+MQ==} + cpu: [arm64] + os: [linux] + + jscpd-linux-arm64-musl@5.3.2: + resolution: {integrity: sha512-+OsVA0iLfDNUIzJArAaJnGyOHxsflpbpkGh/mWs2nZV7vPd1g5Y9SWO+vj83f6BdaO9VNbUbaQAY4dBYadfHIA==} + cpu: [arm64] + os: [linux] + + jscpd-linux-x64-gnu@5.3.2: + resolution: {integrity: sha512-DsNsbCCc5mMOGZdhzgPETsp9RAaKk03cdx1K9wx9RShePnF+AXstYYSiW7M/qjZqvBMlSpO60BXpJvNUQljJJg==} + cpu: [x64] + os: [linux] + + jscpd-linux-x64-musl@5.3.2: + resolution: {integrity: sha512-9D+Jg4MXTHoY5FiK6Fh3FeAr1MsqNS98X5zMUH6PNXu9PKsT0g3Rez2vdYOnoKOE4n8o1XAs8eyU0pm3+HYwog==} + cpu: [x64] + os: [linux] + + jscpd-windows-arm64-msvc@5.3.2: + resolution: {integrity: sha512-qPqZEMHUOjc5tKVU7PElyIhI1J0xyAuQ44S2SwLvKS5qw1NATGTaGhp8MuJkrvTmNcNZmLF0Me6Cv6zj6DVlWA==} + cpu: [arm64] + os: [win32] + + jscpd-windows-x64-msvc@5.3.2: + resolution: {integrity: sha512-WfO1k0/NXA+2HjLaWUkheQv6bEkYrI4WKa/Cf429+dnpdiYB6S8KpJyd6/xGJd+XJ9nKhE0mIFuk3+VdXtaFMQ==} + cpu: [x64] + os: [win32] + + jscpd@5.3.2: + resolution: {integrity: sha512-siYdg3TBNwizWFaYPgr1vwuSs+NWeKJjLAbd00Nkmr2y8TSZRSW0cOGKtqSLgOv7QXDbC/XECnCrZEm5v6RMkg==} + engines: {node: '>=18'} + hasBin: true + json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} @@ -3034,6 +3244,14 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + knip@5.88.1: + resolution: {integrity: sha512-tpy5o7zu1MjawVkLPuahymVJekYY3kYjvzcoInhIchgePxTlo+api90tBv2KfhAIe5uXh+mez1tAfmbv8/TiZg==} + engines: {node: '>=18.18.0'} + hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4 <7' + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -3152,6 +3370,14 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -3292,6 +3518,9 @@ packages: resolution: {integrity: sha512-HfIhB5ruTdQv0XX2XlncWQiJ5SIHZ7NHZhVyHth0CSZ/xzge00etRyYy/3wp/Dsu+PkxMC+6+B2lS/GcKoewkA==} deprecated: Package is no longer supported. Please see https://oslojs.dev for the successor project. + oxc-resolver@11.24.2: + resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3404,6 +3633,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} @@ -3529,6 +3762,9 @@ packages: qrcode-generator@2.0.4: resolution: {integrity: sha512-mZSiP6RnbHl4xL2Ap5HfkjLnmxfKcPWpWe/c+5XxCuetEenqmNFf1FH/ftXPCtFG5/TDobjsjz6sSNL0Sr8Z9g==} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -3636,6 +3872,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -3703,6 +3942,10 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + smol-toml@1.9.0: + resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==} + engines: {node: '>= 18'} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -3786,6 +4029,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -3885,6 +4132,10 @@ packages: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + toad-cache@3.7.1: resolution: {integrity: sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==} engines: {node: '>=20'} @@ -3968,6 +4219,10 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + unbash@2.2.0: + resolution: {integrity: sha512-X2wH19RAPZE3+ldGicOkoj/SIA83OIxcJ6Cuaw23hf8Xc6fQpvZXY0SftE2JgS0QhYLUG4uwodSI3R53keyh7w==} + engines: {node: '>=14'} + undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -4075,6 +4330,10 @@ packages: jsdom: optional: true + walk-up-path@4.0.0: + resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} + engines: {node: 20 || >=22} + web-push@3.6.7: resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} engines: {node: '>= 16'} @@ -4175,6 +4434,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + snapshots: '@aws-crypto/crc32@5.2.0': @@ -4474,6 +4736,12 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@0.45.0': dependencies: tslib: 2.8.1 @@ -4484,6 +4752,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 @@ -4494,6 +4767,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.25.12 @@ -4660,9 +4938,9 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': dependencies: - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -5035,6 +5313,13 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.4 + optional: true + '@noble/ciphers@1.3.0': {} '@noble/hashes@1.8.0': {} @@ -5230,6 +5515,18 @@ snapshots: '@node-rs/bcrypt-win32-ia32-msvc': 1.9.0 '@node-rs/bcrypt-win32-x64-msvc': 1.9.0 + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@opentelemetry/api-logs@0.220.0': dependencies: '@opentelemetry/api': 1.9.1 @@ -5292,6 +5589,67 @@ snapshots: dependencies: '@oslojs/encoding': 0.4.1 + '@oxc-resolver/binding-android-arm-eabi@11.24.2': + optional: true + + '@oxc-resolver/binding-android-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-darwin-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-freebsd-x64@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm-musleabihf@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-arm64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-ppc64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-riscv64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-s390x-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-gnu@11.24.2': + optional: true + + '@oxc-resolver/binding-linux-x64-musl@11.24.2': + optional: true + + '@oxc-resolver/binding-openharmony-arm64@11.24.2': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.24.2': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.24.2': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.24.2': + optional: true + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -5536,6 +5894,11 @@ snapshots: tslib: 2.8.1 optional: true + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + '@tybys/wasm-util@0.8.3': dependencies: tslib: 2.8.1 @@ -5597,7 +5960,6 @@ snapshots: '@types/node@22.20.3': dependencies: undici-types: 6.21.0 - optional: true '@types/nodemailer@8.0.1': dependencies: @@ -5642,15 +6004,15 @@ snapshots: dependencies: '@types/node': 22.19.19 - '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.60.0 - '@typescript-eslint/type-utils': 8.60.0(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.60.0 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -5658,14 +6020,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.60.0 '@typescript-eslint/types': 8.60.0 '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.60.0 debug: 4.4.3 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5688,13 +6050,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.60.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.60.0 '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -5717,13 +6079,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.60.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/utils@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.60.0 '@typescript-eslint/types': 8.60.0 '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -5741,13 +6103,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0))': + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0) + vite: 7.3.6(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) '@vitest/pretty-format@3.2.7': dependencies: @@ -5971,6 +6333,10 @@ snapshots: dependencies: balanced-match: 4.0.4 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + brotli@1.3.3: dependencies: base64-js: 1.5.1 @@ -6329,9 +6695,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4: + eslint@9.39.4(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 @@ -6365,6 +6731,8 @@ snapshots: minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -6429,6 +6797,14 @@ snapshots: fast-fifo@1.3.2: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-json-stringify@6.4.0: @@ -6506,6 +6882,10 @@ snapshots: dependencies: websocket-driver: 0.7.5 + fd-package-json@2.0.0: + dependencies: + walk-up-path: 4.0.0 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -6544,6 +6924,10 @@ snapshots: dependencies: flat-cache: 4.0.1 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + find-my-way@9.9.0: dependencies: fast-deep-equal: 3.1.3 @@ -6611,6 +6995,10 @@ snapshots: safe-buffer: 5.2.1 optional: true + formatly@0.3.0: + dependencies: + fd-package-json: 2.0.0 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 @@ -6700,6 +7088,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -6947,6 +7339,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-number@7.0.0: {} + is-plain-obj@4.1.0: {} is-stream@2.0.1: {} @@ -6965,6 +7359,8 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jiti@2.7.0: {} + jose@5.10.0: {} jose@6.2.12: {} @@ -6979,6 +7375,41 @@ snapshots: dependencies: argparse: 2.0.1 + jscpd-darwin-arm64@5.3.2: + optional: true + + jscpd-darwin-x64@5.3.2: + optional: true + + jscpd-linux-arm64-gnu@5.3.2: + optional: true + + jscpd-linux-arm64-musl@5.3.2: + optional: true + + jscpd-linux-x64-gnu@5.3.2: + optional: true + + jscpd-linux-x64-musl@5.3.2: + optional: true + + jscpd-windows-arm64-msvc@5.3.2: + optional: true + + jscpd-windows-x64-msvc@5.3.2: + optional: true + + jscpd@5.3.2: + optionalDependencies: + jscpd-darwin-arm64: 5.3.2 + jscpd-darwin-x64: 5.3.2 + jscpd-linux-arm64-gnu: 5.3.2 + jscpd-linux-arm64-musl: 5.3.2 + jscpd-linux-x64-gnu: 5.3.2 + jscpd-linux-x64-musl: 5.3.2 + jscpd-windows-arm64-msvc: 5.3.2 + jscpd-windows-x64-msvc: 5.3.2 + json-bigint@1.0.0: dependencies: bignumber.js: 9.3.1 @@ -7034,6 +7465,24 @@ snapshots: dependencies: json-buffer: 3.0.1 + knip@5.88.1(@types/node@22.20.3)(typescript@5.9.3): + dependencies: + '@nodelib/fs.walk': 1.2.8 + '@types/node': 22.20.3 + fast-glob: 3.3.3 + formatly: 0.3.0 + jiti: 2.7.0 + minimist: 1.2.8 + oxc-resolver: 11.24.2 + picocolors: 1.1.1 + picomatch: 4.0.4 + smol-toml: 1.9.0 + strip-json-comments: 5.0.3 + typescript: 5.9.3 + unbash: 2.2.0 + yaml: 2.9.0 + zod: 4.6.5 + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -7150,6 +7599,13 @@ snapshots: fs-monkey: 1.1.0 optional: true + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.52.0: optional: true @@ -7278,6 +7734,28 @@ snapshots: '@node-rs/argon2': 1.7.0 '@node-rs/bcrypt': 1.9.0 + oxc-resolver@11.24.2: + optionalDependencies: + '@oxc-resolver/binding-android-arm-eabi': 11.24.2 + '@oxc-resolver/binding-android-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-arm64': 11.24.2 + '@oxc-resolver/binding-darwin-x64': 11.24.2 + '@oxc-resolver/binding-freebsd-x64': 11.24.2 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.24.2 + '@oxc-resolver/binding-linux-arm64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-arm64-musl': 11.24.2 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-riscv64-musl': 11.24.2 + '@oxc-resolver/binding-linux-s390x-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-gnu': 11.24.2 + '@oxc-resolver/binding-linux-x64-musl': 11.24.2 + '@oxc-resolver/binding-openharmony-arm64': 11.24.2 + '@oxc-resolver/binding-wasm32-wasi': 11.24.2 + '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 + '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -7386,6 +7864,8 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} pino-abstract-transport@3.0.0: @@ -7420,10 +7900,11 @@ snapshots: dependencies: browserify-zlib: 0.2.0 - postcss-load-config@6.0.1(postcss@8.5.28)(tsx@4.23.13)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.28)(tsx@4.23.13)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: + jiti: 2.7.0 postcss: 8.5.28 tsx: 4.23.13 yaml: 2.9.0 @@ -7507,6 +7988,8 @@ snapshots: qrcode-generator@2.0.4: {} + queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} readable-stream@2.3.8: @@ -7640,6 +8123,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.61.0 fsevents: 2.3.3 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -7713,6 +8200,8 @@ snapshots: signal-exit@4.1.0: {} + smol-toml@1.9.0: {} + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -7801,6 +8290,8 @@ snapshots: strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -7959,6 +8450,10 @@ snapshots: tmp@0.2.7: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + toad-cache@3.7.1: {} tr46@0.0.3: @@ -7974,7 +8469,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(postcss@8.5.28)(tsx@4.23.13)(typescript@5.9.3)(yaml@2.9.0): + tsup@8.5.1(jiti@2.7.0)(postcss@8.5.28)(tsx@4.23.13)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.28.2) cac: 6.7.14 @@ -7985,7 +8480,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.28)(tsx@4.23.13)(yaml@2.9.0) + postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.28)(tsx@4.23.13)(yaml@2.9.0) resolve-from: 5.0.0 rollup: 4.61.0 source-map: 0.7.6 @@ -8027,13 +8522,13 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.60.0(eslint@9.39.4)(typescript@5.9.3): + typescript-eslint@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/parser': 8.60.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.60.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.60.0(eslint@9.39.4)(typescript@5.9.3) - eslint: 9.39.4 + '@typescript-eslint/utils': 8.60.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -8044,6 +8539,8 @@ snapshots: ufo@1.6.4: {} + unbash@2.2.0: {} + undici-types@5.26.5: {} undici-types@6.21.0: {} @@ -8076,13 +8573,13 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.4.1 - vite-node@3.2.4(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0): + vite-node@3.2.4(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0) + vite: 7.3.6(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -8097,7 +8594,7 @@ snapshots: - tsx - yaml - vite@7.3.6(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0): + vite@7.3.6(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0): dependencies: esbuild: 0.28.2 fdir: 6.5.0(picomatch@4.0.4) @@ -8108,14 +8605,15 @@ snapshots: optionalDependencies: '@types/node': 22.19.19 fsevents: 2.3.3 + jiti: 2.7.0 tsx: 4.23.13 yaml: 2.9.0 - vitest@3.2.7(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0): + vitest@3.2.7(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0)) + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 @@ -8133,8 +8631,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@22.19.19)(tsx@4.23.13)(yaml@2.9.0) + vite: 7.3.6(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@22.19.19)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.19 @@ -8152,6 +8650,8 @@ snapshots: - tsx - yaml + walk-up-path@4.0.0: {} + web-push@3.6.7: dependencies: asn1.js: 5.4.1 @@ -8239,3 +8739,5 @@ snapshots: readable-stream: 4.7.0 zod@3.25.76: {} + + zod@4.6.5: {} diff --git a/services/api/package.json b/services/api/package.json index 83773a26..41d4f045 100644 --- a/services/api/package.json +++ b/services/api/package.json @@ -112,7 +112,6 @@ "eslint": "^9.16.0", "ioredis-mock": "^8.9.0", "rimraf": "^6.0.1", - "testcontainers": "^11.14.0", "tsup": "^8.3.5", "tsx": "^4.23.13", "typescript": "^5.6.3", diff --git a/services/media-worker/package.json b/services/media-worker/package.json index 71b07c8b..a20be115 100644 --- a/services/media-worker/package.json +++ b/services/media-worker/package.json @@ -45,7 +45,6 @@ "ffmpeg-static": "^5.2.0", "ffprobe-static": "^3.1.0", "rimraf": "^6.0.1", - "testcontainers": "^11.14.0", "tsup": "^8.3.5", "tsx": "^4.23.13", "typescript": "^5.6.3",