11const KEYWORDS_KEY = "kf_keywords" ;
22const FINDINGS_KEY = "kf_findings" ;
3+ const MAX_FINDINGS = 5000 ;
4+ const MAX_KEYWORDS = 50 ;
5+ const MAX_KEYWORD_LENGTH = 50 ;
36
47const DEFAULT_KEYWORDS = [
58 "key" , "api_key" , "apikey" , "api-key" , "secret" , "token" ,
69 "access_token" , "auth" , "credential" , "password" ,
710 "client_id" , "client_secret"
811] ;
912
13+ // Serialize all storage writes to prevent race conditions
14+ let storageQueue = Promise . resolve ( ) ;
15+ function enqueue ( fn ) {
16+ storageQueue = storageQueue . then ( fn , fn ) ;
17+ return storageQueue ;
18+ }
19+
20+ // --- Per-tab alert icon ---
21+ const alertTabs = new Set ( ) ;
22+ let alertIconCache = null ;
23+
24+ async function buildAlertIcons ( ) {
25+ if ( alertIconCache ) return alertIconCache ;
26+ const sizes = [ 16 , 48 ] ;
27+ const imageData = { } ;
28+ for ( const size of sizes ) {
29+ const resp = await fetch ( chrome . runtime . getURL ( `icons/icon${ size } .png` ) ) ;
30+ const blob = await resp . blob ( ) ;
31+ const bitmap = await createImageBitmap ( blob ) ;
32+ const canvas = new OffscreenCanvas ( size , size ) ;
33+ const ctx = canvas . getContext ( "2d" ) ;
34+ ctx . drawImage ( bitmap , 0 , 0 , size , size ) ;
35+ // Red alert dot in top-right
36+ const r = Math . max ( 3 , Math . round ( size * 0.22 ) ) ;
37+ const cx = size - r - 1 ;
38+ const cy = r + 1 ;
39+ ctx . beginPath ( ) ;
40+ ctx . arc ( cx , cy , r , 0 , Math . PI * 2 ) ;
41+ ctx . fillStyle = "#ff4444" ;
42+ ctx . fill ( ) ;
43+ ctx . lineWidth = size >= 48 ? 2 : 1 ;
44+ ctx . strokeStyle = "#0f0f0f" ;
45+ ctx . stroke ( ) ;
46+ imageData [ size ] = ctx . getImageData ( 0 , 0 , size , size ) ;
47+ }
48+ alertIconCache = imageData ;
49+ return imageData ;
50+ }
51+
52+ async function setAlertIcon ( tabId ) {
53+ if ( alertTabs . has ( tabId ) ) return ;
54+ alertTabs . add ( tabId ) ;
55+ try {
56+ const imageData = await buildAlertIcons ( ) ;
57+ await chrome . action . setIcon ( { tabId, imageData } ) ;
58+ } catch { }
59+ }
60+
61+ function resetTabIcon ( tabId ) {
62+ if ( ! alertTabs . delete ( tabId ) ) return ;
63+ try {
64+ chrome . action . setIcon ( {
65+ tabId,
66+ path : { "16" : "icons/icon16.png" , "48" : "icons/icon48.png" , "128" : "icons/icon128.png" }
67+ } ) ;
68+ } catch { }
69+ }
70+
71+ // Reset icon when a tab navigates to a new page
72+ chrome . tabs . onUpdated . addListener ( ( tabId , changeInfo ) => {
73+ if ( changeInfo . status === "loading" ) {
74+ resetTabIcon ( tabId ) ;
75+ }
76+ } ) ;
77+
78+ // Clean up when a tab is closed
79+ chrome . tabs . onRemoved . addListener ( ( tabId ) => {
80+ alertTabs . delete ( tabId ) ;
81+ } ) ;
82+
1083chrome . runtime . onInstalled . addListener ( async ( details ) => {
1184 if ( details . reason === "install" ) {
1285 await chrome . storage . local . set ( {
@@ -18,7 +91,8 @@ chrome.runtime.onInstalled.addListener(async (details) => {
1891
1992chrome . runtime . onMessage . addListener ( ( request , sender , sendResponse ) => {
2093 if ( request . type === "finding" ) {
21- saveFinding ( request . data ) . then ( ( ) => sendResponse ( { ok : true } ) ) ;
94+ if ( sender . tab ?. id ) setAlertIcon ( sender . tab . id ) ;
95+ enqueue ( ( ) => saveFinding ( request . data ) ) . then ( ( ) => sendResponse ( { ok : true } ) ) ;
2296 return true ;
2397 }
2498 if ( request . type === "getKeywords" ) {
@@ -30,19 +104,19 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
30104 return true ;
31105 }
32106 if ( request . type === "addKeyword" ) {
33- addKeyword ( request . keyword ) . then ( ( result ) => sendResponse ( result ) ) ;
107+ enqueue ( ( ) => addKeyword ( request . keyword ) ) . then ( ( result ) => sendResponse ( result ) ) ;
34108 return true ;
35109 }
36110 if ( request . type === "removeKeyword" ) {
37- removeKeyword ( request . keyword ) . then ( ( ) => sendResponse ( { ok : true } ) ) ;
111+ enqueue ( ( ) => removeKeyword ( request . keyword ) ) . then ( ( ) => sendResponse ( { ok : true } ) ) ;
38112 return true ;
39113 }
40114 if ( request . type === "removeFinding" ) {
41- removeFinding ( request . url ) . then ( ( ) => sendResponse ( { ok : true } ) ) ;
115+ enqueue ( ( ) => removeFinding ( request . findingId ) ) . then ( ( ) => sendResponse ( { ok : true } ) ) ;
42116 return true ;
43117 }
44118 if ( request . type === "clearFindings" ) {
45- clearFindings ( ) . then ( ( ) => sendResponse ( { ok : true } ) ) ;
119+ enqueue ( ( ) => clearFindings ( ) ) . then ( ( ) => sendResponse ( { ok : true } ) ) ;
46120 return true ;
47121 }
48122 if ( request . type === "exportFindings" ) {
@@ -60,6 +134,8 @@ async function addKeyword(keyword) {
60134 const keywords = await getKeywords ( ) ;
61135 const normalized = keyword . trim ( ) . toLowerCase ( ) ;
62136 if ( ! normalized ) return { ok : false , error : "Keyword cannot be empty." } ;
137+ if ( normalized . length > MAX_KEYWORD_LENGTH ) return { ok : false , error : `Keyword must be ${ MAX_KEYWORD_LENGTH } characters or fewer.` } ;
138+ if ( keywords . length >= MAX_KEYWORDS ) return { ok : false , error : `Maximum of ${ MAX_KEYWORDS } keywords allowed.` } ;
63139 if ( keywords . includes ( normalized ) ) return { ok : false , error : "Keyword already exists." } ;
64140 keywords . push ( normalized ) ;
65141 await chrome . storage . local . set ( { [ KEYWORDS_KEY ] : keywords } ) ;
@@ -82,17 +158,25 @@ async function saveFinding(finding) {
82158 ( f ) => f . url === finding . url && f . match === finding . match
83159 ) ;
84160 if ( isDuplicate ) return ;
161+
162+ finding . id = crypto . randomUUID ( ) ;
85163 findings . push ( finding ) ;
164+
165+ // Evict oldest findings when cap is exceeded
166+ if ( findings . length > MAX_FINDINGS ) {
167+ findings . splice ( 0 , findings . length - MAX_FINDINGS ) ;
168+ }
169+
86170 await chrome . storage . local . set ( { [ FINDINGS_KEY ] : findings } ) ;
87171
88172 const badgeCount = findings . length ;
89173 chrome . action . setBadgeText ( { text : badgeCount > 0 ? String ( badgeCount ) : "" } ) ;
90174 chrome . action . setBadgeBackgroundColor ( { color : "#e74c3c" } ) ;
91175}
92176
93- async function removeFinding ( url ) {
177+ async function removeFinding ( findingId ) {
94178 const findings = await getFindings ( ) ;
95- const updated = findings . filter ( ( f ) => f . url !== url ) ;
179+ const updated = findings . filter ( ( f ) => f . id !== findingId ) ;
96180 await chrome . storage . local . set ( { [ FINDINGS_KEY ] : updated } ) ;
97181 chrome . action . setBadgeText ( { text : updated . length > 0 ? String ( updated . length ) : "" } ) ;
98182}
0 commit comments