diff --git a/app/api/farcaster/route.ts b/app/api/farcaster/route.ts index b44f19e08c..f08acd815f 100644 --- a/app/api/farcaster/route.ts +++ b/app/api/farcaster/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; +import { publicEnv } from "@/config/env"; import { UrlGuardError, assertPublicUrl, @@ -8,6 +9,10 @@ import { type UrlGuardOptions, } from "@/lib/security/urlGuard"; import { escapeRegExp } from "@/lib/text/regex"; +import { + parseFarcasterResource, + type FarcasterResourceIdentifier, +} from "@/src/services/farcaster/url"; import type { FarcasterCastPreview, FarcasterChannelPreview, @@ -16,14 +21,10 @@ import type { FarcasterProfilePreview, FarcasterUnavailablePreview, } from "@/types/farcaster.types"; -import { - parseFarcasterResource, - type FarcasterResourceIdentifier, -} from "@/src/services/farcaster/url"; const WARPCAST_API_BASE = - process.env.FARCASTER_WARPCAST_API_BASE ?? "https://api.warpcast.com"; -const WARPCAST_API_KEY = process.env.FARCASTER_WARPCAST_API_KEY ?? undefined; + publicEnv.FARCASTER_WARPCAST_API_BASE ?? "https://api.warpcast.com"; +const WARPCAST_API_KEY = publicEnv.FARCASTER_WARPCAST_API_KEY ?? undefined; const CAST_CACHE_TTL_MS = 20 * 60 * 1000; const PROFILE_CACHE_TTL_MS = 24 * 60 * 60 * 1000; @@ -40,7 +41,15 @@ const HTML_ACCEPT_HEADER = const PUBLIC_URL_POLICY: UrlGuardOptions["policy"] = { blockedHosts: ["localhost", "127.0.0.1", "::1"], - blockedHostSuffixes: [".local", ".internal", ".lan", ".intra", ".corp", ".home", ".test"], + blockedHostSuffixes: [ + ".local", + ".internal", + ".lan", + ".intra", + ".corp", + ".home", + ".test", + ], }; const PUBLIC_URL_OPTIONS: UrlGuardOptions = { @@ -53,11 +62,20 @@ type CacheEntry = { }; const castCache = new Map>(); -const profileCache = new Map>(); -const channelCache = new Map>(); +const profileCache = new Map< + string, + CacheEntry +>(); +const channelCache = new Map< + string, + CacheEntry +>(); const frameCache = new Map>(); -const getCacheValue = (cache: Map>, key: string): T | undefined => { +const getCacheValue = ( + cache: Map>, + key: string +): T | undefined => { const entry = cache.get(key); if (!entry) { return undefined; @@ -83,7 +101,9 @@ const setCacheValue = ( }); }; -const createAbortController = (timeoutMs: number): { +const createAbortController = ( + timeoutMs: number +): { controller: AbortController; cancel: () => void; } => { @@ -146,15 +166,13 @@ const fetchWarpcastJson = async ( throw new Error("Warpcast request aborted"); } - throw error instanceof Error - ? error - : new Error("Warpcast request failed"); + throw error instanceof Error ? error : new Error("Warpcast request failed"); } finally { cancel(); } }; - type WarpcastUserResponse = { +type WarpcastUserResponse = { readonly result?: { readonly user?: { readonly fid?: number; @@ -168,21 +186,21 @@ const fetchWarpcastJson = async ( }; }; - type WarpcastCastEmbed = { +type WarpcastCastEmbed = { readonly url?: string; readonly castId?: { readonly fid?: number; readonly hash?: string }; readonly metadata?: { readonly image?: string }; readonly type?: string; }; - type WarpcastCastAuthor = { +type WarpcastCastAuthor = { readonly fid?: number; readonly username?: string; readonly displayName?: string; readonly pfp?: { readonly url?: string }; }; - type WarpcastCastResponse = { +type WarpcastCastResponse = { readonly result?: { readonly cast?: { readonly hash?: string; @@ -206,7 +224,7 @@ const fetchWarpcastJson = async ( }; }; - type WarpcastChannelResponse = { +type WarpcastChannelResponse = { readonly result?: { readonly channel?: { readonly id?: string; @@ -223,7 +241,7 @@ const fetchWarpcastJson = async ( }; }; - type WarpcastFrameResponse = { +type WarpcastFrameResponse = { readonly result?: { readonly frame?: { readonly url?: string; @@ -500,7 +518,10 @@ const extractTitle = (html: string): string | undefined => { return rawTitle.trim(); }; -const resolveUrl = (base: string, value: string | undefined): string | undefined => { +const resolveUrl = ( + base: string, + value: string | undefined +): string | undefined => { if (!value) { return undefined; } @@ -543,9 +564,7 @@ const fetchHtml = async ( return null; } - throw new Error( - `Frame fetch failed with status ${response.status}` - ); + throw new Error(`Frame fetch failed with status ${response.status}`); } const html = await response.text(); @@ -639,7 +658,9 @@ const handleResource = async ( ): Promise => { if (resource.type === "cast") { const preview = await fetchCastPreview(resource); - return preview ?? toUnavailable(resource.canonicalUrl, "Cast not available"); + return ( + preview ?? toUnavailable(resource.canonicalUrl, "Cast not available") + ); } if (resource.type === "profile") { @@ -661,14 +682,20 @@ const handleResource = async ( const isUrlGuardError = (error: unknown): error is UrlGuardError => error instanceof UrlGuardError || - (typeof error === "object" && error !== null && (error as { name?: string }).name === "UrlGuardError"); + (typeof error === "object" && + error !== null && + (error as { name?: string }).name === "UrlGuardError"); const handleGuardError = (error: unknown, fallbackStatus = 400) => { if (isUrlGuardError(error)) { - return NextResponse.json({ error: error.message }, { status: error.statusCode }); + return NextResponse.json( + { error: error.message }, + { status: error.statusCode } + ); } - const message = error instanceof Error ? error.message : "Invalid or forbidden URL"; + const message = + error instanceof Error ? error.message : "Invalid or forbidden URL"; return NextResponse.json({ error: message }, { status: fallbackStatus }); }; @@ -709,7 +736,9 @@ export async function GET(request: NextRequest) { } const message = - error instanceof Error ? error.message : "Unable to resolve Farcaster data"; + error instanceof Error + ? error.message + : "Unable to resolve Farcaster data"; return NextResponse.json({ error: message }, { status: 502 }); } } diff --git a/app/api/pepe/resolve/route.ts b/app/api/pepe/resolve/route.ts index 5fa82b2392..5cfbd7b6ac 100644 --- a/app/api/pepe/resolve/route.ts +++ b/app/api/pepe/resolve/route.ts @@ -1,9 +1,13 @@ -import { NextRequest, NextResponse } from "next/server"; import * as cheerio from "cheerio"; +import { NextRequest, NextResponse } from "next/server"; import { publicEnv } from "@/config/env"; import LruTtlCache from "@/lib/cache/lruTtl"; -import { UrlGuardError, fetchPublicJson, fetchPublicUrl } from "@/lib/security/urlGuard"; +import { + UrlGuardError, + fetchPublicJson, + fetchPublicUrl, +} from "@/lib/security/urlGuard"; const TOKENSCAN_BASE = "https://tokenscan.io/api"; @@ -98,9 +102,7 @@ const cache = new LruTtlCache({ const USER_AGENT = "6529seize-pepe-card/1.0 (+https://6529.io; fetching pepe.wtf previews)"; const IPFS_GATEWAY = trimTrailingSlashes( - publicEnv.IPFS_GATEWAY_ENDPOINT || - process.env.IPFS_GATEWAY || - "https://ipfs.io/ipfs/" + publicEnv.IPFS_GATEWAY_ENDPOINT || "https://ipfs.io/ipfs/" ); function trimTrailingSlashes(value: string): string { @@ -121,13 +123,17 @@ type ScrapeNextDataResult = { metaImages: string[]; }; -async function fetchText(url: string, timeoutMs = 4000): Promise { +async function fetchText( + url: string, + timeoutMs = 4000 +): Promise { try { const response = await fetchPublicUrl( url, { headers: { - accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", }, }, { @@ -232,7 +238,11 @@ function slugifyName(name: string): string { .replaceAll(/-+/g, "-"); } -function deepFindAll(value: unknown, keys: string[], results: unknown[] = []): unknown[] { +function deepFindAll( + value: unknown, + keys: string[], + results: unknown[] = [] +): unknown[] { if (Array.isArray(value)) { for (const item of value) { deepFindAll(item, keys, results); @@ -285,7 +295,7 @@ async function scrapeNextData(url: string): Promise { } } - const raw = $('script#__NEXT_DATA__').first().text(); + const raw = $("script#__NEXT_DATA__").first().text(); if (!raw) { return { nextData: null, metaImages: Array.from(metaImages) }; } @@ -325,7 +335,9 @@ async function tryExtractImageFromDescription( description: string, pageUrl: string ): Promise { - const urls = Array.from(new Set(description.match(/https?:\/\/[^\s"'<>]+/g) ?? [])); + const urls = Array.from( + new Set(description.match(/https?:\/\/[^\s"'<>]+/g) ?? []) + ); for (const rawUrl of urls) { const trimmed = rawUrl.trim(); if (!trimmed) { @@ -385,7 +397,12 @@ async function scrapePepeAssetPage(slug: string): Promise { const names = deepFindAll(nextData, ["name", "title"]); const artists = deepFindAll(nextData, ["artist", "creator"]); const collections = deepFindAll(nextData, ["collection", "family"]); - const images = deepFindAll(nextData, ["image", "thumbnail_url", "imageUrl", "imageURL"]); + const images = deepFindAll(nextData, [ + "image", + "thumbnail_url", + "imageUrl", + "imageURL", + ]); const descriptions = deepFindAll(nextData, ["description", "body"]); const assetCandidates = deepFindAll(nextData, [ @@ -405,7 +422,9 @@ async function scrapePepeAssetPage(slug: string): Promise { const imageCandidate = extractFirstString(images); scraped.image = normalizeImageUrl(imageCandidate, href); - const assetCode = assetCandidates.find((candidate) => isCounterpartyAssetCode(candidate)); + const assetCode = assetCandidates.find((candidate) => + isCounterpartyAssetCode(candidate) + ); if (assetCode) { scraped.asset = assetCode.toUpperCase(); } @@ -418,12 +437,24 @@ async function scrapePepeAssetPage(slug: string): Promise { } } - const seriesValues = deepFindAll(nextData, ["series", "seriesNumber", "series_number"]); - const cardValues = deepFindAll(nextData, ["card", "cardNumber", "card_number"]); + const seriesValues = deepFindAll(nextData, [ + "series", + "seriesNumber", + "series_number", + ]); + const cardValues = deepFindAll(nextData, [ + "card", + "cardNumber", + "card_number", + ]); const seriesCandidate = - parseMaybeNumber(extractFirstString(seriesValues)) ?? parseMaybeNumber(seriesValues[0]) ?? null; + parseMaybeNumber(extractFirstString(seriesValues)) ?? + parseMaybeNumber(seriesValues[0]) ?? + null; const cardCandidate = - parseMaybeNumber(extractFirstString(cardValues)) ?? parseMaybeNumber(cardValues[0]) ?? null; + parseMaybeNumber(extractFirstString(cardValues)) ?? + parseMaybeNumber(cardValues[0]) ?? + null; scraped.series = seriesCandidate; scraped.card = cardCandidate; } @@ -450,7 +481,10 @@ async function tokenscanJson(path: string): Promise { return fetchJson(`${TOKENSCAN_BASE}${path}`, 5000); } -async function getApproxRates(): Promise<{ ethPerBtc: number; ethPerXcp: number }> { +async function getApproxRates(): Promise<{ + ethPerBtc: number; + ethPerXcp: number; +}> { const response = await fetchJson( "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,counterparty,ethereum&vs_currencies=eth", 4000 @@ -496,7 +530,10 @@ function isValidSlug(slug: string): boolean { return /^[a-z0-9-]{1,64}$/.test(slug); } -async function findWikiLink(name?: string, series?: number | null): Promise { +async function findWikiLink( + name?: string, + series?: number | null +): Promise { if (!name) { return null; } @@ -512,14 +549,14 @@ async function findWikiLink(name?: string, series?: number | null): Promise { const href = `https://pepe.wtf/collection/${encodeURIComponent(slug)}`; const { nextData, metaImages } = await scrapeNextData(href); - const name = extractFirstString(deepFindAll(nextData, ["name", "title"])) ?? + const name = + extractFirstString(deepFindAll(nextData, ["name", "title"])) ?? slug.replaceAll("-", " ").replaceAll(/\b\w/g, (c) => c.toUpperCase()); let image = normalizeImageUrl( - extractFirstString(deepFindAll(nextData, ["image", "thumbnail_url", "imageUrl", "imageURL"])), + extractFirstString( + deepFindAll(nextData, ["image", "thumbnail_url", "imageUrl", "imageURL"]) + ), href ); @@ -578,9 +621,13 @@ async function resolveCollection(slug: string): Promise { async function resolveArtist(slug: string): Promise { const href = `https://pepe.wtf/artists/${encodeURIComponent(slug)}`; const { nextData, metaImages } = await scrapeNextData(href); - const name = extractFirstString(deepFindAll(nextData, ["name", "title"])) ?? slug.replaceAll("-", " "); + const name = + extractFirstString(deepFindAll(nextData, ["name", "title"])) ?? + slug.replaceAll("-", " "); let image = normalizeImageUrl( - extractFirstString(deepFindAll(nextData, ["image", "thumbnail_url", "imageUrl", "imageURL"])), + extractFirstString( + deepFindAll(nextData, ["image", "thumbnail_url", "imageUrl", "imageURL"]) + ), href ); @@ -589,7 +636,9 @@ async function resolveArtist(slug: string): Promise { } const collections = Array.from( new Set( - deepFindAll(nextData, ["collection", "family"]).filter((value): value is string => typeof value === "string") + deepFindAll(nextData, ["collection", "family"]).filter( + (value): value is string => typeof value === "string" + ) ) ); @@ -606,9 +655,13 @@ async function resolveArtist(slug: string): Promise { async function resolveSet(slug: string): Promise { const href = `https://pepe.wtf/sets/${encodeURIComponent(slug)}`; const { nextData, metaImages } = await scrapeNextData(href); - const name = extractFirstString(deepFindAll(nextData, ["name", "title"])) ?? slug.replaceAll("-", " "); + const name = + extractFirstString(deepFindAll(nextData, ["name", "title"])) ?? + slug.replaceAll("-", " "); let image = normalizeImageUrl( - extractFirstString(deepFindAll(nextData, ["image", "thumbnail_url", "imageUrl", "imageURL"])), + extractFirstString( + deepFindAll(nextData, ["image", "thumbnail_url", "imageUrl", "imageURL"]) + ), href ); @@ -621,7 +674,9 @@ async function resolveSet(slug: string): Promise { if (seriesMatch) { const seriesNo = Number(seriesMatch[1]); if (Number.isFinite(seriesNo)) { - wiki = await probeWikiUrl(`https://wiki.pepe.wtf/rare-pepes/series-${seriesNo}`); + wiki = await probeWikiUrl( + `https://wiki.pepe.wtf/rare-pepes/series-${seriesNo}` + ); } } @@ -659,41 +714,74 @@ async function resolveAsset(slug: string): Promise { }; } - const [assetInfo, holdersList, dispensersOpen, marketHistBtc, marketHistXcp, rates] = await Promise.all([ + const [ + assetInfo, + holdersList, + dispensersOpen, + marketHistBtc, + marketHistXcp, + rates, + ] = await Promise.all([ tokenscanJson(`/api/asset/${encodeURIComponent(assetCode)}`), tokenscanJson(`/api/holders/${encodeURIComponent(assetCode)}`), - tokenscanJson(`/api/dispensers/${encodeURIComponent(assetCode)}?status=open`), - tokenscanJson(`/api/market/${encodeURIComponent(assetCode)}/BTC/history`), - tokenscanJson(`/api/market/${encodeURIComponent(assetCode)}/XCP/history`), + tokenscanJson( + `/api/dispensers/${encodeURIComponent(assetCode)}?status=open` + ), + tokenscanJson( + `/api/market/${encodeURIComponent(assetCode)}/BTC/history` + ), + tokenscanJson( + `/api/market/${encodeURIComponent(assetCode)}/XCP/history` + ), getApproxRates(), ]); - const holders = Array.isArray(holdersList?.data) ? holdersList.data.length : null; + const holders = Array.isArray(holdersList?.data) + ? holdersList.data.length + : null; let bestAskSats: number | undefined; if (Array.isArray(dispensersOpen?.data)) { const satsValues = dispensersOpen.data - .map((entry: any) => Number(entry?.satoshi_price ?? entry?.satoshirate ?? 0)) + .map((entry: any) => + Number(entry?.satoshi_price ?? entry?.satoshirate ?? 0) + ) .filter((value: number) => Number.isFinite(value) && value > 0); if (satsValues.length) { bestAskSats = Math.min(...satsValues); } } - const lastBtc = Array.isArray(marketHistBtc?.data) && marketHistBtc.data.length ? marketHistBtc.data[0] : null; - const lastXcp = Array.isArray(marketHistXcp?.data) && marketHistXcp.data.length ? marketHistXcp.data[0] : null; + const lastBtc = + Array.isArray(marketHistBtc?.data) && marketHistBtc.data.length + ? marketHistBtc.data[0] + : null; + const lastXcp = + Array.isArray(marketHistXcp?.data) && marketHistXcp.data.length + ? marketHistXcp.data[0] + : null; const lastSaleSats = Number(lastBtc?.price_sats); const lastSaleXcp = Number(lastXcp?.price); - if (!scraped.image && typeof assetInfo?.description === "string" && assetInfo.description) { - const enriched = await tryExtractImageFromDescription(assetInfo.description, href); + if ( + !scraped.image && + typeof assetInfo?.description === "string" && + assetInfo.description + ) { + const enriched = await tryExtractImageFromDescription( + assetInfo.description, + href + ); if (enriched) { scraped.image = enriched; } } - const wikiLink = await findWikiLink(scraped.name ?? assetCode, scraped.series ?? null); + const wikiLink = await findWikiLink( + scraped.name ?? assetCode, + scraped.series ?? null + ); return { kind: "asset", @@ -709,7 +797,9 @@ async function resolveAsset(slug: string): Promise { holders, image: scraped.image ?? null, links: { - horizon: `https://horizon.market/explorer/assets/${encodeURIComponent(assetCode)}`, + horizon: `https://horizon.market/explorer/assets/${encodeURIComponent( + assetCode + )}`, xchain: `https://xchain.io/asset/${encodeURIComponent(assetCode)}`, wiki: wikiLink ?? undefined, }, @@ -725,10 +815,15 @@ async function resolveAsset(slug: string): Promise { } export async function GET(request: NextRequest) { - const kind = (request.nextUrl.searchParams.get("kind") as PepeKind | null) ?? null; + const kind = + (request.nextUrl.searchParams.get("kind") as PepeKind | null) ?? null; const slug = normalizeSlug(request.nextUrl.searchParams.get("slug")); - if (!kind || !slug || !["asset", "collection", "artist", "set"].includes(kind)) { + if ( + !kind || + !slug || + !["asset", "collection", "artist", "set"].includes(kind) + ) { return errorResponse("invalid params", 400); } @@ -761,7 +856,10 @@ export async function GET(request: NextRequest) { return NextResponse.json(preview, { headers: buildCacheHeaders(false) }); } catch (error) { console.error("Failed to resolve pepe preview", error); - return NextResponse.json({ error: "resolve_failed" }, { headers: buildCacheHeaders(false) }); + return NextResponse.json( + { error: "resolve_failed" }, + { headers: buildCacheHeaders(false) } + ); } } diff --git a/components/datePickerModal/DatePickerModal.module.scss b/components/datePickerModal/DatePickerModal.module.scss index 384f65d543..a4b6254859 100644 --- a/components/datePickerModal/DatePickerModal.module.scss +++ b/components/datePickerModal/DatePickerModal.module.scss @@ -1,9 +1,14 @@ @use "../../styles/variables.scss"; .header { - background-color: rgb(40, 40, 40); - color: white; - padding: 10px; + padding: 1rem; + background-color: rgb(40, 40, 40) !important; + color: variables.$font-color !important; + border: 1px solid variables.$lighter-grey !important; + border-radius: 0 !important; + display: flex; + justify-content: space-between; + align-items: center; } .modalClose { @@ -13,17 +18,19 @@ } .body { - background-color: black; - color: white; - border-left: 2px solid rgb(40, 40, 40); - border-right: 2px solid rgb(40, 40, 40); - padding: 20px; + padding: 1rem; + background-color: rgb(40, 40, 40) !important; + color: variables.$font-color !important; + border-top: 0 !important; + border-left: 1px solid variables.$lighter-grey !important; + border-right: 1px solid variables.$lighter-grey !important; + border-radius: 0 !important; } .footer { - border: 2px solid rgb(40, 40, 40); - background-color: black; - color: white; + padding: 1rem; + border: 1px solid variables.$lighter-grey !important; + background-color: rgb(40, 40, 40) !important; + color: white !important; border-radius: 0 !important; - padding: 20px 5px; } diff --git a/components/datePickerModal/DatePickerModal.tsx b/components/datePickerModal/DatePickerModal.tsx index e4ce5bfcf5..df36789c2b 100644 --- a/components/datePickerModal/DatePickerModal.tsx +++ b/components/datePickerModal/DatePickerModal.tsx @@ -1,10 +1,10 @@ "use client"; -import { Button, Col, Container, Form, Modal, Row } from "react-bootstrap"; -import styles from "./DatePickerModal.module.scss"; +import { faTimesCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useEffect, useState } from "react"; -import { faTimesCircle } from "@fortawesome/free-solid-svg-icons"; +import { Button, Col, Container, Form, Modal, Row } from "react-bootstrap"; +import styles from "./DatePickerModal.module.scss"; interface Props { mode: "date" | "block"; @@ -81,14 +81,14 @@ export default function DatePickerModal(props: Readonly) { return ( props.onHide()}> - +
Select {props.mode}s props.onHide()} /> - +
diff --git a/components/drops/view/part/dropPartMarkdown/linkUtils.tsx b/components/drops/view/part/dropPartMarkdown/linkUtils.tsx index 1550569bd9..e6be182d2c 100644 --- a/components/drops/view/part/dropPartMarkdown/linkUtils.tsx +++ b/components/drops/view/part/dropPartMarkdown/linkUtils.tsx @@ -53,15 +53,23 @@ const shouldUseOpenGraphPreview = ( return false; } - if (YOUTUBE_DOMAINS.some((domain) => matchesDomainOrSubdomain(hostname, domain))) { + if ( + YOUTUBE_DOMAINS.some((domain) => matchesDomainOrSubdomain(hostname, domain)) + ) { return false; } - if (TWITTER_DOMAINS.some((domain) => matchesDomainOrSubdomain(hostname, domain))) { + if ( + TWITTER_DOMAINS.some((domain) => matchesDomainOrSubdomain(hostname, domain)) + ) { return false; } - if (ART_BLOCKS_DOMAINS.some((domain) => matchesDomainOrSubdomain(hostname, domain))) { + if ( + ART_BLOCKS_DOMAINS.some((domain) => + matchesDomainOrSubdomain(hostname, domain) + ) + ) { return false; } @@ -72,7 +80,7 @@ const renderExternalOrInternalLink = ( href: string, props: AnchorHTMLAttributes & ExtraProps ) => { - const baseEndpoint = publicEnv.BASE_ENDPOINT || process.env.BASE_ENDPOINT || ""; + const baseEndpoint = publicEnv.BASE_ENDPOINT || ""; const isExternalLink = baseEndpoint && !href.startsWith(baseEndpoint); const { onClick, ...restProps } = props; const anchorProps: AnchorHTMLAttributes & ExtraProps = { @@ -104,4 +112,9 @@ const isValidLink = (href: string): boolean => { return parseUrl(href) !== null || isLikelyEnsTarget(href); }; -export { parseUrl, shouldUseOpenGraphPreview, renderExternalOrInternalLink, isValidLink }; +export { + isValidLink, + parseUrl, + renderExternalOrInternalLink, + shouldUseOpenGraphPreview, +}; diff --git a/config/env.schema.ts b/config/env.schema.ts index 6d060de733..23bd55fe00 100644 --- a/config/env.schema.ts +++ b/config/env.schema.ts @@ -78,6 +78,17 @@ export const publicEnvSchema = z.object({ NEXTGEN_CHAIN_ID: z.coerce.number().int().positive().optional(), USE_DEV_AUTH: z.string().optional(), + /** + * ──────────────── + * FARCASTER CONFIG + * ──────────────── + */ + FARCASTER_WARPCAST_API_BASE: z + .string() + .url("FARCASTER_WARPCAST_API_BASE must be a valid URL") + .optional(), + FARCASTER_WARPCAST_API_KEY: z.string().optional(), + /** * ──────────────── * FEATURES / FLAGS (all optional) diff --git a/helpers/SeizeLinkParser.ts b/helpers/SeizeLinkParser.ts index d7c191ee42..86e7a2838c 100644 --- a/helpers/SeizeLinkParser.ts +++ b/helpers/SeizeLinkParser.ts @@ -26,10 +26,7 @@ export function parseSeizeQueryLink( try { const url = new URL(href); - const allowedOrigins = new Set( - [publicEnv.BASE_ENDPOINT, process.env.BASE_ENDPOINT].filter(Boolean) - ); - + const allowedOrigins = new Set([new URL(publicEnv.BASE_ENDPOINT).origin]); if (!allowedOrigins.has(url.origin)) return null; if (url.pathname !== path) return null; diff --git a/next.config.mjs b/next.config.mjs index 6238e32f9b..09562c772d 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -34,9 +34,9 @@ function createSecurityHeaders(apiEndpoint = "") { ]; } -// ───────────────────────────────────────────────────────────── -// Helpers to remove duplication -// ───────────────────────────────────────────────────────────── +// ─────── +// Helpers +// ─────── const schemaMod = require("./config/env.schema.runtime.cjs"); const { publicEnvSchema } = schemaMod; @@ -105,6 +105,9 @@ function sharedConfig(publicEnv, assetPrefix) { compress: true, productionBrowserSourceMaps: true, sassOptions: { quietDeps: true }, + eslint: { + ignoreDuringBuilds: true, + }, experimental: { webpackMemoryOptimizations: true, webpackBuildWorker: true, @@ -215,6 +218,8 @@ const nextConfigFactory = (phase) => { FEATURE_AB_CARD: publicEnv.FEATURE_AB_CARD, PEPE_CACHE_TTL_MINUTES: publicEnv.PEPE_CACHE_TTL_MINUTES, PEPE_CACHE_MAX_ITEMS: publicEnv.PEPE_CACHE_MAX_ITEMS, + FARCASTER_WARPCAST_API_BASE: publicEnv.FARCASTER_WARPCAST_API_BASE, + FARCASTER_WARPCAST_API_KEY: publicEnv.FARCASTER_WARPCAST_API_KEY, }, async generateBuildId() { return VERSION; diff --git a/package-lock.json b/package-lock.json index de412b5ca5..e6e640edaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "6529seize", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "6529seize", - "version": "0.1.0", + "version": "1.0.0", "dependencies": { "@capacitor/app": "7.0.1", "@capacitor/barcode-scanner": "^2.0.3", diff --git a/package.json b/package.json index ff7d50e58f..551b0cdf1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "6529seize", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { "build:env-schema": "node scripts/build-env-schema.cjs", @@ -8,12 +8,12 @@ "dev": "node scripts/dev-with-fallback.cjs", "compile": "tsc", "generate": "rm -rf tmp_gen_outp && node ./node_modules/@openapitools/openapi-generator-cli/main generate -i openapi.yaml -g typescript -o tmp_gen_outp --additional-properties=modelPropertyNaming=snake_case && rm -rf generated && mkdir generated && mv tmp_gen_outp/models generated/models && rm -rf tmp_gen_outp && mkdir generated/http && echo 'export type HttpFile = any;' > generated/http/http.ts && rm generated/models/all.ts", - "build": "cross-env NODE_OPTIONS=--max-old-space-size=7680 next build --no-lint", - "build:lint": "cross-env NODE_OPTIONS=--max-old-space-size=7680 next build", + "base-build": "cross-env NODE_OPTIONS=--max-old-space-size=7680 next build", + "build": "npm run lint:quiet && npm run base-build", + "build:lint": "npm run prebuild && npm run lint && npm run base-build && npm run postbuild", "prebuild": "npm run build:env-schema && npm run generate", "postbuild": "next-sitemap", "start": "next start -p 3001", - "lint": "next lint", "test": "jest --silent --verbose=false --coverageReporters=none", "test-json": "jest --silent --json --outputFile=test-results/jest-results.json ; npm run test-extract-failed-names", "test-json-changed": "jest --silent --json --outputFile=test-results/jest-results.json --changedSince=main ; npm run test-extract-failed-names", @@ -28,6 +28,8 @@ "test:watch": "nodemon --watch tests --watch src --ext ts,tsx,js,jsx --exec \"npx playwright test\"", "knip": "knip --fix --allow-remove-files", "type-check": "tsc --noEmit -p tsconfig.json", + "lint:quiet": "next lint --quiet", + "lint": "next lint", "lint:fix": "npx eslint . --ext .ts,.tsx,.js,.jsx --fix" }, "dependencies": {