diff --git a/src/api/auth.ts b/src/api/auth.ts index c6f44be..d9389be 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -13,7 +13,7 @@ import type { VerifyEmailRequest, VerifyEmailResponse, } from "@/api/types"; -import { AUTH_ENDPOINTS } from "@/lib/constants"; +import { AUTH_ENDPOINTS, CLIENT_HEADER, CLIENT_HEADER_VALUE } from "@/lib/constants"; import { accessTokenStorage, refreshTokenStorage } from "@/lib/storage"; import { loginResponseSchema, @@ -99,7 +99,7 @@ export async function refreshAccessToken(): Promise { try { const res = await fetch(AUTH_ENDPOINTS.deviceRefresh, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", [CLIENT_HEADER]: CLIENT_HEADER_VALUE }, body: JSON.stringify({ refresh_token: refreshToken }), signal: AbortSignal.timeout(10_000), }); diff --git a/src/api/client.ts b/src/api/client.ts index ee3f9c8..f1af3f3 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,5 +1,5 @@ import type { ApiErrorResponse } from "@/api/types"; -import { AUTH_ENDPOINTS } from "@/lib/constants"; +import { AUTH_ENDPOINTS, CLIENT_HEADER, CLIENT_HEADER_VALUE } from "@/lib/constants"; import { ApiError, isOffline, NetworkError } from "@/lib/errors"; import { accessTokenStorage, @@ -43,7 +43,7 @@ async function refreshAccessToken(): Promise { try { const res = await fetch(AUTH_ENDPOINTS.deviceRefresh, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", [CLIENT_HEADER]: CLIENT_HEADER_VALUE }, body: JSON.stringify({ refresh_token: refreshToken }), }); @@ -88,6 +88,9 @@ export async function request( } } + // Client attribution header (all request() URLs point at the spoo.me backend) + headers[CLIENT_HEADER] = CLIENT_HEADER_VALUE; + // Auth header if (!noAuth) { const authHeader = await getAuthHeader(); diff --git a/src/api/export.ts b/src/api/export.ts index c8e835d..3c4d1a6 100644 --- a/src/api/export.ts +++ b/src/api/export.ts @@ -1,5 +1,5 @@ import type { ExportQuery } from "@/api/types"; -import { API_V1 } from "@/lib/constants"; +import { API_V1, CLIENT_HEADER, CLIENT_HEADER_VALUE } from "@/lib/constants"; import { isOffline, NetworkError } from "@/lib/errors"; import { accessTokenStorage, apiKeyStorage, authModeStorage } from "@/lib/storage"; @@ -17,7 +17,7 @@ export async function exportStats(query: ExportQuery): Promise { } } - const headers: Record = {}; + const headers: Record = { [CLIENT_HEADER]: CLIENT_HEADER_VALUE }; const mode = await authModeStorage.getValue(); if (mode === "apikey") { const key = await apiKeyStorage.getValue(); diff --git a/src/api/stats.ts b/src/api/stats.ts index 981db43..cef117d 100644 --- a/src/api/stats.ts +++ b/src/api/stats.ts @@ -1,6 +1,6 @@ import { request } from "@/api/client"; import type { StatsQuery, StatsResponse } from "@/api/types"; -import { API_BASE_URL, API_V1 } from "@/lib/constants"; +import { API_BASE_URL, API_V1, CLIENT_HEADER, CLIENT_HEADER_VALUE } from "@/lib/constants"; import { ApiError } from "@/lib/errors"; import { statsResponseSchema } from "@/schemas/api"; @@ -50,7 +50,7 @@ export interface V0StatsResponse { */ export async function getStatsV0(shortCode: string, password?: string): Promise { const url = `${API_BASE_URL}/stats/${shortCode}`; - const headers: Record = {}; + const headers: Record = { [CLIENT_HEADER]: CLIENT_HEADER_VALUE }; const body = password ? new URLSearchParams({ password }) : undefined; if (body) { diff --git a/src/entrypoints/background/index.ts b/src/entrypoints/background/index.ts index d20b4a7..48fcb22 100644 --- a/src/entrypoints/background/index.ts +++ b/src/entrypoints/background/index.ts @@ -5,6 +5,8 @@ import { ACCESS_TOKEN_TTL_MS, API_BASE_URL, AUTH_ENDPOINTS, + CLIENT_HEADER, + CLIENT_HEADER_VALUE, HISTORY_MAX_ITEMS, QR_BRAND, TOKEN_REFRESH_BUFFER_MS, @@ -139,7 +141,7 @@ async function handleTokenRefresh(): Promise { async function exchangeDeviceCode(code: string): Promise { const res = await fetch(AUTH_ENDPOINTS.deviceToken, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", [CLIENT_HEADER]: CLIENT_HEADER_VALUE }, body: JSON.stringify({ code }), }); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 9e45ad0..d29517e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -4,6 +4,10 @@ export const QR_API_BASE_URL = import.meta.env.VITE_QR_API_BASE_URL || "https:// export const API_V1 = `${API_BASE_URL}/api/v1`; export const QR_API_V1 = `${QR_API_BASE_URL}/api/v1`; +/** Identifies this client to the spoo.me API. Never sent to third-party hosts. */ +export const CLIENT_HEADER = "X-Spoo-Client"; +export const CLIENT_HEADER_VALUE = `snap/${browser.runtime.getManifest().version}`; + export const AUTH_ENDPOINTS = { login: `${API_BASE_URL}/auth/login`, register: `${API_BASE_URL}/auth/register`,