Compare commits
	
		
			3 Commits
		
	
	
		
			8fc1b33977
			...
			fa42e848ea
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fa42e848ea | |||
| 29d1131a6f | |||
| b53f0b3bb0 | 
							
								
								
									
										24
									
								
								memory.ts
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								memory.ts
									
									
									
									
									
								
							| @ -39,12 +39,24 @@ class Memory extends EventEmitter { | ||||
|   } | ||||
| 
 | ||||
|   async removeCookiesFor(origin: string, shorthost?: string): Promise<void> { | ||||
|     const clusters = this.getClustersForOrigin(origin); | ||||
|     await Promise.all( | ||||
|       Object.values(clusters) | ||||
|         .filter((cluster) => !shorthost || cluster.id === shorthost) | ||||
|         .map((cluster) => cluster.removeAllCookies()) | ||||
|     ); | ||||
|     if (shorthost) { | ||||
|       const cookies = await browser.cookies.getAll({ domain: shorthost }); | ||||
|       for (const cookie of cookies) { | ||||
|         console.log("removing cookie", cookie.name, "from", cookie.domain); | ||||
|         await browser.cookies.remove({ | ||||
|           name: cookie.name, | ||||
|           url: `https://${cookie.domain}`, | ||||
|         }); | ||||
|       } | ||||
|     } else { | ||||
|       const clusters = this.getClustersForOrigin(origin); | ||||
| 
 | ||||
|       await Promise.all( | ||||
|         Object.values(clusters) | ||||
|           .filter((cluster) => !shorthost || cluster.id === shorthost) | ||||
|           .map((cluster) => this.removeCookiesFor(origin, cluster.id)) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async removeRequestsFor(origin: string) { | ||||
|  | ||||
							
								
								
									
										31
									
								
								options.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								options.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| import React from "react"; | ||||
| 
 | ||||
| export default function Options({ | ||||
|   minValueLength, | ||||
|   setMinValueLength, | ||||
|   cookiesOnly, | ||||
|   setCookiesOnly, | ||||
| }) { | ||||
|   return ( | ||||
|     <fieldset> | ||||
|       <h3>Zaawansowane ustawienia</h3> | ||||
|       <label htmlFor="minValueLength"> | ||||
|         Pokazuj tylko wartości o długości co najmniej{" "} | ||||
|       </label> | ||||
|       <input | ||||
|         type="number" | ||||
|         id="minValueLength" | ||||
|         value={minValueLength} | ||||
|         onChange={(e) => setMinValueLength(parseInt(e.target.value))} | ||||
|       /> | ||||
|       <br /> | ||||
|       <input | ||||
|         type="checkbox" | ||||
|         id="cookiesOnly" | ||||
|         value={cookiesOnly} | ||||
|         onChange={(e) => setCookiesOnly(e.target.checked)} | ||||
|       /> | ||||
|       <label htmlFor="cookiesOnly">Pokazuj tylko dane z cookiesów</label> | ||||
|     </fieldset> | ||||
|   ); | ||||
| } | ||||
| @ -24,11 +24,11 @@ export class StolenDataEntry { | ||||
|     public name: string, | ||||
|     public value: string | ||||
|   ) { | ||||
|     try { | ||||
|       this.iab = TCString.decode(value); | ||||
|       // console.log(this.iab);
 | ||||
|       this.isIAB = true; | ||||
|     } catch (e) {} | ||||
|     // try {
 | ||||
|     //   this.iab = TCString.decode(value);
 | ||||
|     //   // console.log(this.iab);
 | ||||
|     //   this.isIAB = true;
 | ||||
|     // } catch (e) {}
 | ||||
|     this.id = id.next().value as number; | ||||
|   } | ||||
| 
 | ||||
| @ -41,6 +41,49 @@ export class StolenDataEntry { | ||||
|     } | ||||
|     return priority; | ||||
|   } | ||||
| 
 | ||||
|   mergeWith(entry: StolenDataEntry): MergedStolenDataEntry { | ||||
|     return new MergedStolenDataEntry([this, entry]); | ||||
|   } | ||||
| 
 | ||||
|   hasValue(value: string) { | ||||
|     return this.value === value; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export class MergedStolenDataEntry { | ||||
|   constructor(public entries: StolenDataEntry[]) {} | ||||
| 
 | ||||
|   hasValue(value: string) { | ||||
|     return this.entries.some((entry) => entry.value === value); | ||||
|   } | ||||
| 
 | ||||
|   mergeWith(entry: StolenDataEntry) { | ||||
|     this.entries.push(entry); | ||||
|     return this; | ||||
|   } | ||||
| 
 | ||||
|   getPriority() { | ||||
|     return Math.max(...this.entries.map((entry) => entry.getPriority())); | ||||
|   } | ||||
| 
 | ||||
|   getUniqueKey() { | ||||
|     return `${this.getNames().join(":")};${this.entries | ||||
|       .map((e) => e.id) | ||||
|       .join(":")};`;
 | ||||
|   } | ||||
| 
 | ||||
|   getNames(): string[] { | ||||
|     return Array.from(new Set(this.entries.map((e) => e.name))); | ||||
|   } | ||||
| 
 | ||||
|   getSources(): string[] { | ||||
|     return Array.from(new Set(this.entries.map((e) => e.source))); | ||||
|   } | ||||
| 
 | ||||
|   getValues() { | ||||
|     return Array.from(new Set(this.entries.map((e) => e.value))); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export class RequestCluster extends EventEmitter { | ||||
| @ -65,7 +108,7 @@ export class RequestCluster extends EventEmitter { | ||||
|   getStolenData(filter: { | ||||
|     minValueLength: number; | ||||
|     cookiesOnly: boolean; | ||||
|   }): StolenDataEntry[] { | ||||
|   }): MergedStolenDataEntry[] { | ||||
|     return this.requests | ||||
|       .map((request) => request.getAllStolenData()) | ||||
|       .reduce((a, b) => a.concat(b), []) | ||||
| @ -73,11 +116,9 @@ export class RequestCluster extends EventEmitter { | ||||
|         return entry.value.length >= filter.minValueLength; | ||||
|       }) | ||||
|       .filter((entry) => !filter.cookiesOnly || entry.source === "cookie") | ||||
|       .sort((entry1, entry2) => | ||||
|         entry1.getPriority() > entry2.getPriority() ? -1 : 1 | ||||
|       ) | ||||
|       .sort((entryA, entryB) => (entryA.name > entryB.name ? -1 : 1)) | ||||
|       .filter((element, index, array) => { | ||||
|         // remove duplicate neighbours
 | ||||
|         // remove duplicates by name/value
 | ||||
|         if (index == 0) { | ||||
|           return true; | ||||
|         } | ||||
| @ -87,7 +128,24 @@ export class RequestCluster extends EventEmitter { | ||||
|         ) { | ||||
|           return true; | ||||
|         } | ||||
|       }); | ||||
|       }) | ||||
|       .sort((entryA, entryB) => (entryA.value > entryB.value ? -1 : 1)) | ||||
|       .reduce( | ||||
|         (acc: MergedStolenDataEntry[], entry: StolenDataEntry) => { | ||||
|           // group by value
 | ||||
|           const last_entry = acc.slice(-1)[0]; | ||||
|           if (last_entry.hasValue(entry.value)) { | ||||
|             last_entry.mergeWith(entry); | ||||
|           } else { | ||||
|             acc.push(new MergedStolenDataEntry([entry])); | ||||
|           } | ||||
|           return acc; | ||||
|         }, | ||||
|         [new MergedStolenDataEntry([])] as MergedStolenDataEntry[] | ||||
|       ) | ||||
|       .sort((entry1, entry2) => | ||||
|         entry1.getPriority() > entry2.getPriority() ? -1 : 1 | ||||
|       ); | ||||
|   } | ||||
| 
 | ||||
|   static sortCompare(a: RequestCluster, b: RequestCluster) { | ||||
| @ -105,15 +163,4 @@ export class RequestCluster extends EventEmitter { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async removeAllCookies() { | ||||
|     const cookies = await browser.cookies.getAll({ domain: this.id }); | ||||
|     for (const cookie of cookies) { | ||||
|       console.log("removing cookie", cookie.name, "from", cookie.domain); | ||||
|       await browser.cookies.remove({ | ||||
|         name: cookie.name, | ||||
|         url: `https://${cookie.domain}`, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										185
									
								
								sidebar.tsx
									
									
									
									
									
								
							
							
						
						
									
										185
									
								
								sidebar.tsx
									
									
									
									
									
								
							| @ -1,8 +1,9 @@ | ||||
| import React, { useEffect, useState } from "react"; | ||||
| import ReactDOM from "react-dom"; | ||||
| import memory from "./memory"; | ||||
| import { RequestCluster, Sources } from "./request-cluster"; | ||||
| import { getshorthost, useEmitter } from "./util"; | ||||
| import Options from "./options"; | ||||
| import { StolenData } from "./stolen-data"; | ||||
| import { useEmitter } from "./util"; | ||||
| 
 | ||||
| async function getCurrentTab() { | ||||
|   const [tab] = await browser.tabs.query({ | ||||
| @ -12,189 +13,11 @@ async function getCurrentTab() { | ||||
|   return tab; | ||||
| } | ||||
| 
 | ||||
| const TabDropdown = ({ | ||||
|   setPickedTab, | ||||
|   pickedTab, | ||||
| }: { | ||||
|   setPickedTab: (tab_id: number) => void; | ||||
|   pickedTab: number; | ||||
| }) => { | ||||
|   const [tabs, setTabs] = useState([]); | ||||
|   useEffect(() => { | ||||
|     browser.tabs.query({ currentWindow: true }).then(setTabs); | ||||
|   }, []); | ||||
|   return ( | ||||
|     <select | ||||
|       id="tab_dropdown" | ||||
|       value={pickedTab} | ||||
|       onChange={async (e) => { | ||||
|         setPickedTab(parseInt(e.target.value)); | ||||
|       }} | ||||
|     > | ||||
|       {tabs.map((tab) => ( | ||||
|         <option value={tab.id} key={tab.id}> | ||||
|           {tab.title} | ||||
|         </option> | ||||
|       ))} | ||||
|     </select> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const StolenDataRow = ({ | ||||
|   origin, | ||||
|   shorthost, | ||||
|   minValueLength, | ||||
|   cookiesOnly, | ||||
| }: { | ||||
|   origin: string; | ||||
|   shorthost: string; | ||||
|   refreshToken: number; | ||||
|   minValueLength: number; | ||||
|   cookiesOnly: boolean; | ||||
| }) => { | ||||
|   const cluster = memory.getClustersForOrigin(origin)[shorthost]; | ||||
|   const icons: Record<Sources, string> = { | ||||
|     cookie: "🍪", | ||||
|     pathname: "🛣", | ||||
|     queryparams: "🅿", | ||||
|     header: "H", | ||||
|   }; | ||||
|   return ( | ||||
|     <div> | ||||
|       <h2> | ||||
|         {cluster.id} {cluster.hasCookies() ? "🍪" : ""} x | ||||
|         {cluster.requests.length}{" "} | ||||
|         <a | ||||
|           href="#" | ||||
|           style={{ fontSize: "10px" }} | ||||
|           onClick={() => cluster.removeAllCookies()} | ||||
|         > | ||||
|           Wyczyść cookiesy | ||||
|         </a> | ||||
|       </h2> | ||||
|       <table> | ||||
|         <tbody> | ||||
|           {cluster | ||||
|             .getStolenData({ minValueLength, cookiesOnly }) | ||||
|             .map((entry) => ( | ||||
|               <tr | ||||
|                 key={ | ||||
|                   origin + ";" + cluster.id + ";" + entry.id + ";" + entry.name | ||||
|                 } | ||||
|                 data-key={ | ||||
|                   origin + ";" + cluster.id + ";" + entry.id + ";" + entry.name | ||||
|                 } | ||||
|               > | ||||
|                 <th style={{ maxWidth: "200px", wordWrap: "break-word" }}> | ||||
|                   {entry.name} | ||||
|                 </th> | ||||
|                 <td>{icons[entry.source]}</td> | ||||
|                 <td style={{ wordWrap: "anywhere" as any }}> | ||||
|                   {entry.value} {entry.isIAB ? "!!!!! IAB" : ""} | ||||
|                 </td> | ||||
|               </tr> | ||||
|             ))} | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const StolenData = ({ | ||||
|   origin, | ||||
|   minValueLength, | ||||
|   refreshToken, | ||||
|   cookiesOnly, | ||||
| }: { | ||||
|   origin: string; | ||||
|   refreshToken: number; | ||||
|   minValueLength: number; | ||||
|   cookiesOnly: boolean; | ||||
| }) => { | ||||
|   if (!origin) { | ||||
|     return <div></div>; | ||||
|   } | ||||
|   const clusters = Object.values(memory.getClustersForOrigin(origin)).sort( | ||||
|     RequestCluster.sortCompare | ||||
|   ); | ||||
|   return ( | ||||
|     <div style={{ padding: "5px" }}> | ||||
|       {" "} | ||||
|       <div> | ||||
|         <h1> | ||||
|           {origin} | ||||
|           <button | ||||
|             style={{ marginLeft: "1rem" }} | ||||
|             onClick={() => | ||||
|               memory.removeCookiesFor( | ||||
|                 origin, | ||||
|                 getshorthost(new URL(origin).host) | ||||
|               ) | ||||
|             } | ||||
|           > | ||||
|             Wyczyść cookiesy 1st party | ||||
|           </button> | ||||
|           <button | ||||
|             style={{ marginLeft: "1rem" }} | ||||
|             onClick={() => memory.removeRequestsFor(origin)} | ||||
|           > | ||||
|             Wyczyść pamięć | ||||
|           </button> | ||||
|         </h1> | ||||
|         {clusters | ||||
|           .filter((cluster) => !cookiesOnly || cluster.hasCookies()) | ||||
|           .map((cluster) => { | ||||
|             return ( | ||||
|               <StolenDataRow | ||||
|                 origin={origin} | ||||
|                 shorthost={cluster.id} | ||||
|                 key={cluster.id + origin} | ||||
|                 refreshToken={refreshToken} | ||||
|                 minValueLength={minValueLength} | ||||
|                 cookiesOnly={cookiesOnly} | ||||
|               /> | ||||
|             ); | ||||
|           })} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const Options = ({ | ||||
|   minValueLength, | ||||
|   setMinValueLength, | ||||
|   cookiesOnly, | ||||
|   setCookiesOnly, | ||||
| }) => { | ||||
|   return ( | ||||
|     <fieldset> | ||||
|       <h3>Zaawansowane ustawienia</h3> | ||||
|       <label htmlFor="minValueLength"> | ||||
|         Pokazuj tylko wartości o długości co najmniej{" "} | ||||
|       </label> | ||||
|       <input | ||||
|         type="number" | ||||
|         id="minValueLength" | ||||
|         value={minValueLength} | ||||
|         onChange={(e) => setMinValueLength(parseInt(e.target.value))} | ||||
|       /> | ||||
|       <br /> | ||||
|       <input | ||||
|         type="checkbox" | ||||
|         id="cookiesOnly" | ||||
|         value={cookiesOnly} | ||||
|         onChange={(e) => setCookiesOnly(e.target.checked)} | ||||
|       /> | ||||
|       <label htmlFor="cookiesOnly">Pokazuj tylko dane z cookiesów</label> | ||||
|     </fieldset> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const Sidebar = () => { | ||||
|   const [origin, setOrigin] = useState<string | null>(null); | ||||
|   const [minValueLength, setMinValueLength] = useState<number | null>(7); | ||||
|   const [cookiesOnly, setCookiesOnly] = useState<boolean>(false); | ||||
|   const counter = useEmitter(memory); | ||||
|   const [counter] = useEmitter(memory); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     const listener = async (data) => { | ||||
|  | ||||
							
								
								
									
										59
									
								
								stolen-data-row.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								stolen-data-row.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| import React from "react"; | ||||
| import memory from "./memory"; | ||||
| import { Sources } from "./request-cluster"; | ||||
| 
 | ||||
| export default function StolenDataRow({ | ||||
|   origin, | ||||
|   shorthost, | ||||
|   minValueLength, | ||||
|   cookiesOnly, | ||||
| }: { | ||||
|   origin: string; | ||||
|   shorthost: string; | ||||
|   refreshToken: number; | ||||
|   minValueLength: number; | ||||
|   cookiesOnly: boolean; | ||||
| }) { | ||||
|   const cluster = memory.getClustersForOrigin(origin)[shorthost]; | ||||
|   const icons: Record<Sources, string> = { | ||||
|     cookie: "🍪", | ||||
|     pathname: "🛣", | ||||
|     queryparams: "🅿", | ||||
|     header: "H", | ||||
|   }; | ||||
|   return ( | ||||
|     <div> | ||||
|       <h2> | ||||
|         {cluster.id} {cluster.hasCookies() ? "🍪" : ""} x | ||||
|         {cluster.requests.length}{" "} | ||||
|         <a | ||||
|           href="#" | ||||
|           style={{ fontSize: "10px" }} | ||||
|           onClick={() => memory.removeCookiesFor(origin, shorthost)} | ||||
|         > | ||||
|           Wyczyść cookiesy | ||||
|         </a> | ||||
|       </h2> | ||||
|       <table> | ||||
|         <tbody> | ||||
|           {cluster | ||||
|             .getStolenData({ minValueLength, cookiesOnly }) | ||||
|             .map((entry) => ( | ||||
|               <tr | ||||
|                 key={origin + cluster.id + entry.getUniqueKey()} | ||||
|                 data-key={origin + cluster.id + entry.getUniqueKey()} | ||||
|               > | ||||
|                 <th style={{ maxWidth: "200px", wordWrap: "break-word" }}> | ||||
|                   {entry.getNames().join(",")} | ||||
|                 </th> | ||||
|                 <td>{entry.getSources().map((source) => icons[source])}</td> | ||||
|                 <td style={{ wordWrap: "anywhere" as any }}> | ||||
|                   {entry.getValues()[0]} | ||||
|                 </td> | ||||
|               </tr> | ||||
|             ))} | ||||
|         </tbody> | ||||
|       </table> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										65
									
								
								stolen-data.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								stolen-data.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| import React from "react"; | ||||
| import memory from "./memory"; | ||||
| import { RequestCluster } from "./request-cluster"; | ||||
| import StolenDataRow from "./stolen-data-row"; | ||||
| import { getshorthost } from "./util"; | ||||
| 
 | ||||
| export function StolenData({ | ||||
|   origin, | ||||
|   minValueLength, | ||||
|   refreshToken, | ||||
|   cookiesOnly, | ||||
| }: { | ||||
|   origin: string; | ||||
|   refreshToken: number; | ||||
|   minValueLength: number; | ||||
|   cookiesOnly: boolean; | ||||
| }) { | ||||
|   if (!origin) { | ||||
|     return <div></div>; | ||||
|   } | ||||
|   const clusters = Object.values(memory.getClustersForOrigin(origin)).sort( | ||||
|     RequestCluster.sortCompare | ||||
|   ); | ||||
|   return ( | ||||
|     <div style={{ padding: "5px" }}> | ||||
|       {" "} | ||||
|       <div> | ||||
|         <h1> | ||||
|           {origin} | ||||
|           <button | ||||
|             style={{ marginLeft: "1rem" }} | ||||
|             onClick={() => | ||||
|               memory.removeCookiesFor( | ||||
|                 origin, | ||||
|                 getshorthost(new URL(origin).host) | ||||
|               ) | ||||
|             } | ||||
|           > | ||||
|             Wyczyść cookiesy 1st party | ||||
|           </button> | ||||
|           <button | ||||
|             style={{ marginLeft: "1rem" }} | ||||
|             onClick={() => memory.removeRequestsFor(origin)} | ||||
|           > | ||||
|             Wyczyść pamięć | ||||
|           </button> | ||||
|         </h1> | ||||
|         {clusters | ||||
|           .filter((cluster) => !cookiesOnly || cluster.hasCookies()) | ||||
|           .map((cluster) => { | ||||
|             return ( | ||||
|               <StolenDataRow | ||||
|                 origin={origin} | ||||
|                 shorthost={cluster.id} | ||||
|                 key={cluster.id + origin} | ||||
|                 refreshToken={refreshToken} | ||||
|                 minValueLength={minValueLength} | ||||
|                 cookiesOnly={cookiesOnly} | ||||
|               /> | ||||
|             ); | ||||
|           })} | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										30
									
								
								tab-dropdown.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								tab-dropdown.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| import React from "react"; | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| export default function TabDropdown({ | ||||
|   setPickedTab, | ||||
|   pickedTab, | ||||
| }: { | ||||
|   setPickedTab: (tab_id: number) => void; | ||||
|   pickedTab: number; | ||||
| }) { | ||||
|   const [tabs, setTabs] = useState([]); | ||||
|   useEffect(() => { | ||||
|     browser.tabs.query({ currentWindow: true }).then(setTabs); | ||||
|   }, []); | ||||
|   return ( | ||||
|     <select | ||||
|       id="tab_dropdown" | ||||
|       value={pickedTab} | ||||
|       onChange={async (e) => { | ||||
|         setPickedTab(parseInt(e.target.value)); | ||||
|       }} | ||||
|     > | ||||
|       {tabs.map((tab) => ( | ||||
|         <option value={tab.id} key={tab.id}> | ||||
|           {tab.title} | ||||
|         </option> | ||||
|       ))} | ||||
|     </select> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										8
									
								
								util.ts
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								util.ts
									
									
									
									
									
								
							| @ -1,5 +1,5 @@ | ||||
| import { EventEmitter } from "events"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { Dispatch, SetStateAction, useEffect, useState } from "react"; | ||||
| 
 | ||||
| export type Unpromisify<T> = T extends Promise<infer R> ? R : T; | ||||
| export type Unarray<T> = T extends Array<infer R> ? R : T; | ||||
| @ -13,7 +13,9 @@ export function getshorthost(host: string) { | ||||
|   return host.split(".").slice(-2).join("."); | ||||
| } | ||||
| 
 | ||||
| export function useEmitter(e: EventEmitter) { | ||||
| export function useEmitter( | ||||
|   e: EventEmitter | ||||
| ): [number, Dispatch<SetStateAction<number>>] { | ||||
|   const [counter, setCounter] = useState<number>(0); | ||||
|   useEffect(() => { | ||||
|     const callback = () => { | ||||
| @ -24,7 +26,7 @@ export function useEmitter(e: EventEmitter) { | ||||
|       e.removeListener("change", callback); | ||||
|     }; | ||||
|   }, []); | ||||
|   return counter; | ||||
|   return [counter, setCounter]; | ||||
| } | ||||
| 
 | ||||
| export function parseCookie(cookie: string): Record<string, string> { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user