From 6d2a61f91c7b87ac8d2f58094964f94415c0e970 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Thu, 20 Aug 2026 11:12:29 -0600 Subject: [PATCH] Add a local-dev fallback for download device IDs crypto.randomUUID is unavailable on HTTP, which breaks download counting during local development. Production still uses the native UUID. Co-authored-by: Cursor --- src/components/Hack/HackActions.tsx | 9 +++----- src/components/Hack/VersionList.tsx | 9 +++----- src/utils/deviceId.ts | 35 +++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 src/utils/deviceId.ts diff --git a/src/components/Hack/HackActions.tsx b/src/components/Hack/HackActions.tsx index 1c133f3..9c4c974 100644 --- a/src/components/Hack/HackActions.tsx +++ b/src/components/Hack/HackActions.tsx @@ -13,6 +13,7 @@ import { isArchiveFile, isAnyRomExtension, } from "@/utils/romFile"; +import { getOrCreateDeviceId } from "@/utils/deviceId"; import { collectFetchDiagnostics, HTTPError, runConnectivityProbes } from "@/utils/patches/download-telemetry"; import type { SelectablePatch } from "@/types/patcher"; import { applyPatch, patchFormatFromFilename, type PatchFormat } from "@/utils/patching"; @@ -436,12 +437,8 @@ const HackActions: React.FC = ({ try { const countPatchId = selectedPatch?.id ?? patchId; if (countPatchId != null) { - const key = "deviceId"; - let deviceId = localStorage.getItem(key); - if (!deviceId) { - deviceId = crypto.randomUUID(); - localStorage.setItem(key, deviceId); - } + const deviceId = getOrCreateDeviceId(); + if (!deviceId) return; // Defer count update to avoid Safari cancelling the request setTimeout(async () => { const deviceIdObscured = deviceId.split("-"); diff --git a/src/components/Hack/VersionList.tsx b/src/components/Hack/VersionList.tsx index cff9852..72496a0 100644 --- a/src/components/Hack/VersionList.tsx +++ b/src/components/Hack/VersionList.tsx @@ -7,6 +7,7 @@ import { FiEdit2, FiEdit, FiX } from "react-icons/fi"; import VersionActions from "@/components/Hack/VersionActions"; import type { PatchesDownloadPermission } from "@/components/Hack/DownloadPermissionSettings"; import { updatePatchChangelog, updatePatchVersion, getPatchDownloadUrl, updatePatchDownloadCount } from "@/app/hack/[slug]/actions"; +import { getOrCreateDeviceId } from "@/utils/deviceId"; import { useRouter } from "next/navigation"; import { createClient } from "@/utils/supabase/client"; import type { Patch } from "@/components/Hack/PatcherVersionManager"; @@ -38,12 +39,8 @@ function PublicPatchDownloadButton({ patchId }: { patchId: number }) { window.open(result.url, "_blank"); // Best-effort log download for counting try { - const key = "deviceId"; - let deviceId = localStorage.getItem(key); - if (!deviceId) { - deviceId = crypto.randomUUID(); - localStorage.setItem(key, deviceId); - } + const deviceId = getOrCreateDeviceId(); + if (!deviceId) return; setTimeout(async () => { const deviceIdObscured = deviceId.split("-"); const countResult = await updatePatchDownloadCount(patchId, deviceIdObscured); diff --git a/src/utils/deviceId.ts b/src/utils/deviceId.ts new file mode 100644 index 0000000..9b4e08d --- /dev/null +++ b/src/utils/deviceId.ts @@ -0,0 +1,35 @@ +const DEVICE_ID_STORAGE_KEY = "deviceId"; + +function createDeviceId(): string | null { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + // Insecure-context local dev only. Must stay five hyphen-separated segments. + if (process.env.NODE_ENV === "development") { + return `dev-${Date.now()}-0-0-0`; + } + return null; +} + +function isDevFallbackId(id: string): boolean { + return id.startsWith("dev-"); +} + +export function getOrCreateDeviceId(): string | null { + const existing = localStorage.getItem(DEVICE_ID_STORAGE_KEY); + const isStaleDevFallback = + !!existing && isDevFallbackId(existing) && process.env.NODE_ENV !== "development"; + + if (existing && !isStaleDevFallback) return existing; + + const created = createDeviceId(); + if (created) { + localStorage.setItem(DEVICE_ID_STORAGE_KEY, created); + return created; + } + + if (isStaleDevFallback) { + localStorage.removeItem(DEVICE_ID_STORAGE_KEY); + } + return null; +}