chore: merge main and release 3.7.3 bugfix bundle

Resolve conflicts with deep-reform (PR #452) while keeping bugfix branch
changes: sidebar visibility, verbose logging, device login name, and
notification archive. Bump to 3.7.3 with What's New release notes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 20:12:58 +09:30
93 changed files with 2628 additions and 1368 deletions
+151 -69
View File
@@ -10,8 +10,10 @@ import {
performCloudSettingsUploadWithRetry,
requestCloudSettingsDebouncedUpload,
runCloudSettingsPoll,
withSuppressedCloudAutoUpload,
} from "./background/cloudSettingsAutoSync";
import { getBsplusDeviceName } from "@/seqta/utils/bsplusDeviceName";
import { isAllowedFetchUrl } from "@/seqta/utils/allowedFetchUrl";
/**
* Session-only dev-mode override of the content API base.
@@ -46,6 +48,11 @@ function reloadSeqtaPages() {
/** Callback for sending a response back to the message sender */
type MessageSender = { (response?: unknown): void };
async function getAccessTokenFromStorage(): Promise<string | null> {
const { bsplus_token } = await browser.storage.local.get("bsplus_token");
return typeof bsplus_token === "string" && bsplus_token.length > 0 ? bsplus_token : null;
}
/** Accept API + GitHub fallback shapes; always return `{ success, data?: { themes } }`. */
function normalizeFetchThemesResponse(json: unknown): {
success: boolean;
@@ -80,66 +87,101 @@ function normalizeFetchThemesResponse(json: unknown): {
}
function handleFetchThemes(request: any, sendResponse: MessageSender): boolean {
const { token } = request;
const apiUrl = `${apiBase()}/api/themes?type=betterseqta&limit=100&nocache=${Date.now()}`;
const githubUrl = `https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes.json?nocache=${Date.now()}`;
const headers: Record<string, string> = {};
if (token) headers["Authorization"] = `Bearer ${token}`;
fetch(apiUrl, { cache: "no-store", headers })
.then(async (r) => {
const json = await r.json();
if (!r.ok) {
throw new Error(
(json && typeof json === "object" && "error" in json && typeof (json as { error?: string }).error === "string"
? (json as { error: string }).error
: null) ?? `Themes API HTTP ${r.status}`,
);
}
return normalizeFetchThemesResponse(json);
})
.then(sendResponse)
.catch((err) => {
console.warn("[Background] fetchThemes API failed, trying GitHub fallback:", err?.message);
fetch(githubUrl, { cache: "no-store" })
.then(async (r) => {
if (!r.ok) throw new Error(`GitHub fallback HTTP ${r.status}`);
const data = await r.json();
const themes = Array.isArray(data) ? data : (data?.themes ?? []);
return normalizeFetchThemesResponse({ success: true, data: { themes } });
})
.then(sendResponse)
.catch((fallbackErr) => {
console.error("[Background] fetchThemes GitHub fallback error:", fallbackErr);
sendResponse({ success: false, error: fallbackErr?.message });
});
});
void (async () => {
const token = await getAccessTokenFromStorage();
const apiUrl = `${apiBase()}/api/themes?type=betterseqta&limit=100&nocache=${Date.now()}`;
const githubUrl = `https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes.json?nocache=${Date.now()}`;
const headers: Record<string, string> = {};
if (token) headers["Authorization"] = `Bearer ${token}`;
fetch(apiUrl, { cache: "no-store", headers })
.then(async (r) => {
const json = await r.json();
if (!r.ok) {
throw new Error(
(json && typeof json === "object" && "error" in json && typeof (json as { error?: string }).error === "string"
? (json as { error: string }).error
: null) ?? `Themes API HTTP ${r.status}`,
);
}
return normalizeFetchThemesResponse(json);
})
.then(sendResponse)
.catch((err) => {
console.warn("[Background] fetchThemes API failed, trying GitHub fallback:", err?.message);
fetch(githubUrl, { cache: "no-store" })
.then(async (r) => {
if (!r.ok) throw new Error(`GitHub fallback HTTP ${r.status}`);
const data = await r.json();
const themes = Array.isArray(data) ? data : (data?.themes ?? []);
return normalizeFetchThemesResponse({ success: true, data: { themes } });
})
.then(sendResponse)
.catch((fallbackErr) => {
console.error("[Background] fetchThemes GitHub fallback error:", fallbackErr);
sendResponse({ success: false, error: fallbackErr?.message });
});
});
})();
return true;
}
function handleFetchThemeDetails(request: any, sendResponse: MessageSender): boolean {
const { themeId, token } = request;
const { themeId } = request;
if (!themeId || typeof themeId !== "string") {
sendResponse({ success: false, error: "Missing themeId" });
return false;
}
const headers: Record<string, string> = {};
if (token) headers["Authorization"] = `Bearer ${token}`;
fetch(`${apiBase()}/api/themes/${themeId}`, { cache: "no-store", headers })
.then((r) => r.json())
.then(sendResponse)
.catch((err) => {
console.error("[Background] fetchThemeDetails error:", err);
sendResponse({ success: false, error: err?.message });
});
void (async () => {
const token = await getAccessTokenFromStorage();
const headers: Record<string, string> = {};
if (token) headers["Authorization"] = `Bearer ${token}`;
fetch(`${apiBase()}/api/themes/${themeId}`, { cache: "no-store", headers })
.then((r) => r.json())
.then(sendResponse)
.catch((err) => {
console.error("[Background] fetchThemeDetails error:", err);
sendResponse({ success: false, error: err?.message });
});
})();
return true;
}
function handleFetchFromUrl(request: any, sendResponse: MessageSender): boolean {
function isTrustedSender(sender?: browser.Runtime.MessageSender): boolean {
if (!sender) return false;
if (sender.id && sender.id !== browser.runtime.id) return false;
const urls = [sender.url, sender.tab?.url].filter(Boolean) as string[];
for (const pageUrl of urls) {
if (/^chrome-extension:\/\//.test(pageUrl) || /^moz-extension:\/\//.test(pageUrl)) {
return true;
}
try {
if (isSeqtaOrigin(new URL(pageUrl).origin)) return true;
} catch {
// try next URL
}
}
return false;
}
function handleFetchFromUrl(
request: any,
sendResponse: MessageSender,
sender?: browser.Runtime.MessageSender,
): boolean {
if (!isTrustedSender(sender)) {
sendResponse({ error: "Unauthorized sender" });
return false;
}
const { url } = request;
if (!url || typeof url !== "string") {
sendResponse({ error: "Missing url" });
return false;
}
if (!isAllowedFetchUrl(url)) {
sendResponse({ error: "URL not allowed" });
return false;
}
fetch(url, { cache: "no-store" })
.then((r) => r.json())
.then((data) => sendResponse({ data }))
@@ -178,7 +220,15 @@ function handleCloudReserveClient(request: any, sendResponse: MessageSender): bo
return true;
}
function handleCloudLogin(request: any, sendResponse: MessageSender): boolean {
function handleCloudLogin(
request: any,
sendResponse: MessageSender,
sender?: browser.Runtime.MessageSender,
): boolean {
if (!isTrustedSender(sender)) {
sendResponse({ error: "Unauthorized sender" });
return false;
}
const { client_id, redirect_uri, login, password, device_name } = request;
if (!client_id || !redirect_uri || !login || !password) {
sendResponse({ error: "Missing client_id, redirect_uri, login, or password" });
@@ -300,10 +350,18 @@ function handleCloudRefresh(request: any, sendResponse: MessageSender): boolean
return true;
}
function handleCloudSettingsUpload(request: any, sendResponse: MessageSender): boolean {
function handleCloudSettingsUpload(
request: any,
sendResponse: MessageSender,
sender?: browser.Runtime.MessageSender,
): boolean {
if (!isTrustedSender(sender)) {
sendResponse({ success: false, error: "Unauthorized sender" });
return false;
}
void (async () => {
try {
const token = request.token as string | undefined;
const token = await getAccessTokenFromStorage();
if (!token) {
sendResponse({ success: false, error: "Not authenticated" });
return;
@@ -325,10 +383,18 @@ function handleCloudSettingsUpload(request: any, sendResponse: MessageSender): b
return true;
}
function handleCloudSettingsDownload(request: any, sendResponse: MessageSender): boolean {
function handleCloudSettingsDownload(
request: any,
sendResponse: MessageSender,
sender?: browser.Runtime.MessageSender,
): boolean {
if (!isTrustedSender(sender)) {
sendResponse({ success: false, error: "Unauthorized sender" });
return false;
}
void (async () => {
try {
const token = request.token as string | undefined;
const token = await getAccessTokenFromStorage();
if (!token) {
sendResponse({ success: false, error: "Not authenticated" });
return;
@@ -352,22 +418,29 @@ function handleCloudSettingsDownload(request: any, sendResponse: MessageSender):
}
function handleCloudFavorite(request: any, sendResponse: MessageSender): boolean {
const { themeId, token, action } = request;
if (!themeId || !token) {
sendResponse({ success: false, error: "Theme ID and token required" });
const { themeId, action } = request;
if (!themeId) {
sendResponse({ success: false, error: "Theme ID required" });
return false;
}
const isFavorite = action === "favorite";
fetch(`${apiBase()}/api/themes/${themeId}/favorite`, {
method: isFavorite ? "POST" : "DELETE",
headers: { Authorization: `Bearer ${token}` },
})
.then((r) => r.json())
.then(sendResponse)
.catch((err) => {
console.error("[Background] cloudFavorite error:", err);
sendResponse({ success: false, error: err?.message });
});
void (async () => {
const token = await getAccessTokenFromStorage();
if (!token) {
sendResponse({ success: false, error: "Not authenticated" });
return;
}
const isFavorite = action === "favorite";
fetch(`${apiBase()}/api/themes/${themeId}/favorite`, {
method: isFavorite ? "POST" : "DELETE",
headers: { Authorization: `Bearer ${token}` },
})
.then((r) => r.json())
.then(sendResponse)
.catch((err) => {
console.error("[Background] cloudFavorite error:", err);
sendResponse({ success: false, error: err?.message });
});
})();
return true;
}
@@ -385,7 +458,12 @@ function isSeqtaOrigin(origin: string): boolean {
}
}
function handleSetDevApiBase(request: any): boolean {
function handleSetDevApiBase(
request: any,
_sendResponse: MessageSender,
sender?: browser.Runtime.MessageSender,
): boolean {
if (!isTrustedSender(sender)) return false;
const url = typeof request?.url === "string" ? request.url.trim() : null;
if (url && /^https?:\/\//.test(url)) {
DEV_API_BASE = url.replace(/\/$/, "");
@@ -424,7 +502,11 @@ const MESSAGE_HANDLERS: Record<string, MessageHandler> = {
});
return true;
},
sendNews: (req, sendResponse) => {
sendNews: (req, sendResponse, sender) => {
if (!isTrustedSender(sender)) {
sendResponse({ error: "Unauthorized sender" });
return false;
}
fetchNews(req.source ?? "australia", sendResponse);
return true;
},
@@ -501,10 +583,10 @@ function getDefaultValues(): SettingsState {
return getDefaultSettingsState();
}
function SetStorageValue(object: any) {
for (var i in object) {
browser.storage.local.set({ [i]: object[i] });
}
function SetStorageValue(object: SettingsState) {
void withSuppressedCloudAutoUpload(() =>
browser.storage.local.set(object as Record<string, unknown>),
);
}
/** One-time migration for 3.6.5: opt upgraders into Global Search + indexing + transparency defaults. */