Skip to content

Commit a2975d8

Browse files
committed
feat: add extension bridge key management and desktop pet widget
- Implemented commands for generating, retrieving, and rotating an extension bridge key using UUIDs and HMAC-SHA256 for signature verification. - Added a new PetWidget component that displays a customizable desktop pet with state management, including focus and idle states, and interaction messages. - Introduced a fallback manifest for the pet widget to ensure graceful degradation when manifest data is missing.
1 parent a575275 commit a2975d8

38 files changed

Lines changed: 1313 additions & 54 deletions

CHANGELOG.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,58 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

66
---
77

8+
## [1.4.0] - 2026-05-16
9+
10+
### Added
11+
12+
#### Extension Bridge Authentication
13+
14+
- **Extension bridge key management** — the desktop app now generates a shared bridge key on first launch, stores it locally, and exposes it in Settings with copy and rotate actions
15+
- **Capability negotiation for extension auth** — VS Code and browser extensions now probe the desktop API before signing requests, so old app versions without bridge auth support do not receive key-protected traffic
16+
- **VS Code extension key entry** — the extension now includes an input command and settings key for saving, updating, and reusing the bridge key
17+
- **Browser extension key entry** — the browser popup now keeps the bridge key field visible, auto-fills the saved key, and allows users to modify and re-save it at any time
18+
19+
#### Desktop Pet / Widget Center
20+
21+
- **Desktop pet widget** — added a built-in manifest-driven desktop pet widget with idle / focus / rest states and tap messages
22+
- **Pet resource pack import** — users can import JSON pet packs and apply the manifest to all existing pet widgets from a prominent entry in Widget Center
23+
- **Pet size controls** — Widget Center now exposes a visible pet settings panel for adjusting default pet window width and height in bulk
24+
- **Pet default sizing** — the default pet window size was increased to better match the richer pet UI
25+
26+
#### Data Migration and Compatibility
27+
28+
- **Legacy database migration** — Windows startup now detects the old Roaming data directory and automatically copies the legacy database into the new app data location, including SQLite `-wal` / `-shm` sidecar files
29+
- **Fallback compatibility**[Crucial] older data directories remain readable during the transition so existing users do not hit a blank or empty state after the 1.2.0 path change
30+
31+
#### Widget Center / Registry
32+
33+
- **Widget Center pet section** — the widget marketplace now surfaces the pet widget more prominently alongside the official widget catalog
34+
- **Widget registry alignment** — built-in pet registry sizing was aligned with the new default pet window dimensions
35+
36+
### Changed
37+
38+
#### Local API and Sync Flow
39+
40+
- **Local API status payload expanded**`/api/status` now advertises whether extension bridge authentication is required so clients can decide whether to send signed traffic
41+
- **Signed request flow tightened** — both extensions only attach signatures when the desktop API explicitly declares support, preventing bridge-key traffic from reaching older app builds
42+
43+
#### Settings UX
44+
45+
- **Settings bridge section** — the desktop Settings page now shows the bridge key in full, supports one-click copy, and lets users rotate the key without leaving the page
46+
- **Browser and VS Code key UX** — both extensions now emphasize that the bridge key can be updated after initial save instead of being a one-time setup
47+
48+
#### Default Widget Behavior
49+
50+
- **Pet widget startup size** — the pet widget defaults were updated in both registry metadata and tray-based widget creation paths so new windows open at a more natural size
51+
52+
### Fixed
53+
54+
- Fixed browser and VS Code extensions sending bridge signatures to older desktop API versions that did not support key-based auth
55+
- Fixed pet widget sizing being too tight for the richer built-in manifest-based UI
56+
- Fixed legacy Windows users from landing on an empty database path after the app data migration change
57+
- Fixed the browser extension key field being hidden after save, which made updating the saved key awkward
58+
59+
860
## [1.2.0] - 2026-05-10
961

1062
### Added

browser-extension (2).zip

33.8 KB
Binary file not shown.

browser-extension/_locales/en/messages.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,18 @@
33
"message": "TimeLens Browser Companion"
44
},
55
"extensionDescription": {
6-
"message": "Companion extension for TimeLens. Tracks active tab sessions, detects idle time, and syncs browser usage data to the local desktop app."
6+
"message": "TimeLens browser companion that tracks tab sessions and syncs browser usage to the desktop app."
77
},
88
"extensionActionTitle": {
99
"message": "TimeLens Browser Companion"
10+
},
11+
"extensionBridgeKey": {
12+
"message": "Extension bridge key"
13+
},
14+
"bridgeKeyHint": {
15+
"message": "Get the key from TimeLens Settings > Local API / Extension Bridge."
16+
},
17+
"save": {
18+
"message": "Save"
1019
}
1120
}

browser-extension/_locales/zh_CN/messages.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,14 @@
77
},
88
"extensionActionTitle": {
99
"message": "TimeLens 浏览器伴侣"
10+
},
11+
"extensionBridgeKey": {
12+
"message": "扩展网桥密钥"
13+
},
14+
"bridgeKeyHint": {
15+
"message": "从 TimeLens 设置 > 本地 API / 扩展网桥中获取密钥。"
16+
},
17+
"save": {
18+
"message": "保存"
1019
}
1120
}

browser-extension/background.js

Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const STORAGE_KEYS = {
2626
const MAX_RECENT_SESSIONS = 100;
2727
const MAX_PENDING_SESSIONS = 200;
2828
const API_BASE = "http://127.0.0.1:49152";
29+
let authRequiredCache = { value: false, expiresAt: 0 };
2930

3031
// Consider the user idle after 60 s without mouse/keyboard input.
3132
const IDLE_THRESHOLD_SECONDS = 60;
@@ -274,21 +275,31 @@ async function flushPendingSessions() {
274275

275276
async function syncSessionToDesktop(session) {
276277
try {
278+
const bridgeKey = await getBridgeKey();
279+
const body = JSON.stringify({
280+
browser_name: session.browserName,
281+
tab_url: session.url,
282+
host: session.host || "",
283+
title: session.title || "",
284+
started_at: new Date(session.startedAt).toISOString(),
285+
ended_at: new Date(session.endedAt).toISOString(),
286+
duration_seconds: Math.max(0, Math.round((session.durationMs || 0) / 1000)),
287+
locale: session.locale || getLocale(),
288+
});
289+
290+
const headers = {
291+
"Content-Type": "application/json",
292+
};
293+
294+
// Only attach signature when desktop API explicitly requires bridge auth.
295+
if (bridgeKey && await shouldAttachBridgeSignature()) {
296+
headers["X-Extension-Signature"] = await signRequestBody(body, bridgeKey);
297+
}
298+
277299
const response = await fetch(`${API_BASE}/api/browser/session`, {
278300
method: "POST",
279-
headers: {
280-
"Content-Type": "application/json",
281-
},
282-
body: JSON.stringify({
283-
browser_name: session.browserName,
284-
tab_url: session.url,
285-
host: session.host || "",
286-
title: session.title || "",
287-
started_at: new Date(session.startedAt).toISOString(),
288-
ended_at: new Date(session.endedAt).toISOString(),
289-
duration_seconds: Math.max(0, Math.round((session.durationMs || 0) / 1000)),
290-
locale: session.locale || getLocale(),
291-
}),
301+
headers,
302+
body,
292303
});
293304

294305
if (!response.ok) {
@@ -342,3 +353,64 @@ async function pingApiStatus() {
342353
});
343354
}
344355
}
356+
357+
/**
358+
* Old desktop APIs do not expose auth capability. In that case default to false
359+
* and do not send key/signature headers.
360+
*/
361+
async function shouldAttachBridgeSignature() {
362+
const now = Date.now();
363+
if (authRequiredCache.expiresAt > now) {
364+
return authRequiredCache.value;
365+
}
366+
367+
try {
368+
const response = await fetch(`${API_BASE}/api/status`);
369+
if (!response.ok) {
370+
authRequiredCache = { value: false, expiresAt: now + 30_000 };
371+
return false;
372+
}
373+
const data = await response.json();
374+
const required = data?.extension_bridge_auth_required === true;
375+
authRequiredCache = { value: required, expiresAt: now + 30_000 };
376+
return required;
377+
} catch {
378+
authRequiredCache = { value: false, expiresAt: now + 15_000 };
379+
return false;
380+
}
381+
}
382+
383+
/**
384+
* Get the stored extension bridge key from local storage
385+
*/
386+
async function getBridgeKey() {
387+
const { "timelens.bridgeKey": key } = await chrome.storage.local.get("timelens.bridgeKey");
388+
return key || "";
389+
}
390+
391+
/**
392+
* Sign a request body using HMAC-SHA256
393+
* @param {string} body - The request body JSON string
394+
* @param {string} key - The bridge key
395+
* @returns {Promise<string>} The hex-encoded HMAC signature
396+
*/
397+
async function signRequestBody(body, key) {
398+
// Use SubtleCrypto API available in Service Workers
399+
const encoder = new TextEncoder();
400+
const keyData = encoder.encode(key);
401+
const bodyData = encoder.encode(body);
402+
403+
const cryptoKey = await crypto.subtle.importKey(
404+
"raw",
405+
keyData,
406+
{ name: "HMAC", hash: "SHA-256" },
407+
false,
408+
["sign"]
409+
);
410+
411+
const signature = await crypto.subtle.sign("HMAC", cryptoKey, bodyData);
412+
const signatureArray = new Uint8Array(signature);
413+
return Array.from(signatureArray)
414+
.map((byte) => byte.toString(16).padStart(2, "0"))
415+
.join("");
416+
}

browser-extension/i18n.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ const TRANSLATIONS = {
2222
activeTab: "Active tab",
2323
unknownSite: "Unknown site",
2424
extensionDisabled: "Desktop app has browser sync disabled.",
25+
extensionBridgeKey: "Extension bridge key",
26+
bridgeKeyHint: "Get the key from TimeLens Settings > Local API / Extension Bridge.",
27+
bridgeKeyPlaceholder: "Paste extension bridge key from TimeLens Settings",
28+
save: "Save",
29+
saveOrUpdate: "Save / Update",
30+
bridgeKeySaved: "Extension bridge key saved successfully.",
31+
bridgeKeyCleared: "Extension bridge key cleared.",
2532
},
2633
"zh-CN": {
2734
connection: "桌面端连接",
@@ -46,6 +53,13 @@ const TRANSLATIONS = {
4653
activeTab: "活动标签页",
4754
unknownSite: "未知站点",
4855
extensionDisabled: "桌面端已关闭浏览器同步。",
56+
extensionBridgeKey: "扩展网桥密钥",
57+
bridgeKeyHint: "从 TimeLens 设置 > 本地 API / 扩展网桥中获取密钥。",
58+
bridgeKeyPlaceholder: "粘贴从 TimeLens 设置中获取的扩展网桥密钥",
59+
save: "保存",
60+
saveOrUpdate: "保存/更新",
61+
bridgeKeySaved: "扩展网桥密钥已保存。",
62+
bridgeKeyCleared: "扩展网桥密钥已清除。",
4963
},
5064
};
5165

browser-extension/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "__MSG_extensionName__",
4-
"version": "1.0.0",
4+
"version": "1.1.0",
55
"description": "__MSG_extensionDescription__",
66
"default_locale": "en",
77
"permissions": ["storage", "tabs", "alarms", "idle"],

browser-extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "timelens-browser-extension",
33
"version": "1.1.0",
4-
"description": "Browser companion for TimeLens — tracks tab sessions and syncs screen-time data with the desktop app.",
4+
"description": "TimeLens browser companion that tracks tab sessions and syncs browser usage to the desktop app.",
55
"scripts": {
66
"lint": "web-ext lint --source-dir .",
77
"build": "web-ext build --source-dir . --artifacts-dir dist --overwrite-dest",

browser-extension/popup.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ <h2 data-i18n="connection">Desktop connection</h2>
2222
<p id="status-text" class="muted">Trying to reach the local TimeLens API.</p>
2323
</section>
2424

25+
<section id="bridge-key-panel" class="panel">
26+
<div class="panel-head">
27+
<h2 data-i18n="extensionBridgeKey">Extension bridge key</h2>
28+
</div>
29+
<div style="display: flex; gap: 8px;">
30+
<input
31+
id="bridge-key-input"
32+
type="password"
33+
placeholder="Paste extension bridge key from TimeLens Settings"
34+
style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px;"
35+
/>
36+
<button id="save-key-button" class="ghost-button" type="button" data-i18n="saveOrUpdate">Save / Update</button>
37+
</div>
38+
<p class="muted" style="margin-top: 8px; font-size: 11px;" data-i18n="bridgeKeyHint">Get the key from TimeLens Settings > Local API / Extension Bridge.</p>
39+
</section>
40+
2541
<section class="panel">
2642
<div class="panel-head">
2743
<h2 data-i18n="today">Today in TimeLens</h2>

browser-extension/popup.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,40 @@ const todayList = document.querySelector("#today-list");
1515
const recentList = document.querySelector("#recent-list");
1616
const activeTabPill = document.querySelector("#active-tab-pill");
1717
const refreshButton = document.querySelector("#refresh-button");
18+
const bridgeKeyPanel = document.querySelector("#bridge-key-panel");
19+
const bridgeKeyInput = document.querySelector("#bridge-key-input");
20+
const saveKeyButton = document.querySelector("#save-key-button");
1821
const locale = getLocale();
1922

2023
applyStaticTranslations();
2124

25+
if (bridgeKeyInput) {
26+
bridgeKeyInput.placeholder = t("bridgeKeyPlaceholder", {}, locale);
27+
}
28+
29+
// Setup bridge key UI
30+
if (saveKeyButton) {
31+
saveKeyButton.addEventListener("click", async () => {
32+
const key = bridgeKeyInput?.value.trim() || "";
33+
await chrome.storage.local.set({ "timelens.bridgeKey": key });
34+
if (key) {
35+
alert(t("bridgeKeySaved", {}, locale));
36+
} else {
37+
alert(t("bridgeKeyCleared", {}, locale));
38+
}
39+
});
40+
}
41+
42+
// Load saved bridge key into input so users can modify it anytime.
43+
async function loadBridgeKey() {
44+
const { "timelens.bridgeKey": savedKey } = await chrome.storage.local.get("timelens.bridgeKey");
45+
if (bridgeKeyInput) {
46+
bridgeKeyInput.value = savedKey || "";
47+
}
48+
}
49+
50+
void loadBridgeKey();
51+
2252
refreshButton.addEventListener("click", () => {
2353
loadAll();
2454
});

0 commit comments

Comments
 (0)