fix: add validation and improve error handling in sync operations

- Hide API keys from error messages by stripping query strings
- Validate that category responses contain entries before using them
- Distinguish between unreadable and malformed cookie values with proper types
This commit is contained in:
Tautvydas Derzinskas committed 2026-10-20 00:17:12 +02:00
1 parent 27c42fe790
commit 7c3c7ec902
2 files changed
+17 -6

No files matched your search

+6 -2
View File
@@ -47,7 +47,8 @@ async function json(
init: { method?: "GET" | "POST"; body?: string; headers?: Record<string, string> } = {},
) {
const res = await browserFetch(url, { ...init, headers: { Accept: "application/json", ...init.headers } });
if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`);
// Without the query string, which can carry an API key.
if (!res.ok) throw new Error(`${url.split("?")[0]}: HTTP ${res.status}`);
return res.json() as Promise<unknown>;
}
@@ -178,7 +179,10 @@ const FETCHERS: Record<string, () => Promise<Entry[]>> = {
let failed = false;
for (const [provider, fetch] of Object.entries(FETCHERS)) {
try {
catalog[provider] = await fetch();
const entries = await fetch();
// A changed payload shape reads as no categories rather than an error.
if (!entries.length) throw new Error("no categories in the response");
catalog[provider] = entries;
console.log(`${provider}: ${catalog[provider].length} categories`);
} catch (err) {
failed = true;
+11 -4
View File
@@ -8,13 +8,19 @@ import { apiCall, requireConfig } from "./api";
const SITE = "https://thangs.com";
const JWT = /^eyJ[\w-]+\.eyJ[\w-]+\.[\w-]+$/;
async function cookieValue(name: string): Promise<string> {
/** The cookie's token, "" when there is none, or undefined when it couldn't be read. */
async function cookieValue(name: string): Promise<string | undefined> {
let raw: string | undefined;
try {
const cookie = await chrome.cookies.get({ url: SITE, name });
const value = cookie?.value ? decodeURIComponent(cookie.value) : "";
raw = (await chrome.cookies.get({ url: SITE, name }))?.value;
} catch {
return undefined; // Not readable: no reason to think the user signed out.
}
try {
const value = raw ? decodeURIComponent(raw) : "";
return JWT.test(value) ? value : "";
} catch {
return ""; // Unreadable, or malformed percent-encoding: not a token.
return ""; // Malformed percent-encoding: not a token.
}
}
@@ -23,6 +29,7 @@ export async function syncThangsSession(): Promise<void> {
const config = await requireConfig().catch(() => null);
if (!config) return;
const [token, refreshToken] = await Promise.all([cookieValue("accessToken"), cookieValue("refreshToken")]);
if (token === undefined || refreshToken === undefined) return;
// Per Thingport account, so switching the extension to another one sends it there too.
const account = `${config.instanceUrl} ${config.email}`;
const last = config.lastSyncedThangsSession;