typecheck fixes checkpoint

This commit is contained in:
Kuba Orlik 2022-07-09 15:28:37 +02:00
parent c86a3e2c23
commit e1d97f0411
15 changed files with 107 additions and 101 deletions

View File

@ -39,10 +39,12 @@ export default function EmailContent({
function copyTextToClipboard() { function copyTextToClipboard() {
// Should be changed in the future to Clipboard API (https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write#browser_compatibility) // Should be changed in the future to Clipboard API (https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write#browser_compatibility)
let r = document.createRange(); let r = document.createRange();
r.selectNode(document.querySelector('.mail-container__content')); const container = document.querySelector('.mail-container__content');
window.getSelection().addRange(r); if (!container) return;
r.selectNode(container);
window.getSelection()?.addRange(r);
document.execCommand('copy'); document.execCommand('copy');
window.getSelection().removeAllRanges(); window.getSelection()?.removeAllRanges();
setCopy(true); setCopy(true);
} }

View File

@ -122,7 +122,10 @@ function generateHostPage(
defaultValueExpression: defaultValueExpression:
index == 0 index == 0
? 'marketing' ? 'marketing'
: `{${f('legitimate_interest_description', previous_cluster)}}`, : `{${f(
'legitimate_interest_description',
previous_cluster || undefined
)}}`,
}, },
{ {
type: 'radiogroup', type: 'radiogroup',

View File

@ -16,7 +16,9 @@ function handleNewFile(
); );
setFiltered(new Blob([JSON.stringify(content)], { type: 'application/json' })); setFiltered(new Blob([JSON.stringify(content)], { type: 'application/json' }));
}); });
reader.readAsText(element.files[0]); const file = element?.files?.[0];
if (!file) throw new Error('file empty?');
reader.readAsText(file);
} }
function generateFakeHAR(entries: StolenDataEntry[]) { function generateFakeHAR(entries: StolenDataEntry[]) {
@ -80,8 +82,11 @@ export default function HARConverter({ entries }: { entries: StolenDataEntry[] }
type="file" type="file"
accept=".har" accept=".har"
onChange={(e) => { onChange={(e) => {
setFilename(e.target.files[0].name); const file = e.target?.files?.[0];
if (file) {
setFilename(file.name);
handleNewFile(e.target, entries, setFiltered); handleNewFile(e.target, entries, setFiltered);
}
}} }}
/> />
{(filtered && ( {(filtered && (

View File

@ -1,36 +0,0 @@
import { RequestCluster } from '../../../request-cluster';
import { ExplainerKey } from '../explainers';
import { ParsedAnswers, ParsedHostAnswers } from '../parse-answers';
import { v } from '../verbs';
import { Problem } from './problem';
type UnlawfulDataClassification = 'no_purpose';
export function classifyUnlawfulData(
hostAnswers: ParsedHostAnswers,
cluster: RequestCluster
): UnlawfulDataClassification {
if (hostAnswers.present == 'not_mentioned' && hostAnswers.was_processing_necessary == 'no') {
return 'no_purpose';
}
}
export class UnlawfulData extends Problem {
static qualifies(answers: ParsedAnswers, clusters: RequestCluster[]): boolean {}
getEmailContent() {
const _ = (key: string) => v(key, this.answers.zaimek);
return (
<>
<h2>Przetwarzanie danych osobowych bez ważnej podsawy prawnej</h2>
<p>
{_('Moje')} dane osobowe zostały ujawnione podmiotom, które właścicielami
domen:
</p>
{this.getRangeDescription()}
</>
);
}
getNecessaryExplainers() {
return [] as ExplainerKey[];
}
}

View File

@ -22,15 +22,17 @@ function Report() {
const url = new URL(document.location.toString()); const url = new URL(document.location.toString());
const origin = url.searchParams.get('origin'); const origin = url.searchParams.get('origin');
const [counter] = useEmitter(getMemory()); const [counter] = useEmitter(getMemory());
const rawAnswers = url.searchParams.get('answers');
const [answers, setAnswers] = React.useState<ParsedAnswers>( const [answers, setAnswers] = React.useState<ParsedAnswers>(
url.searchParams.get('answers') ? JSON.parse(url.searchParams.get('answers')) : null rawAnswers ? JSON.parse(rawAnswers) : null
); );
const [mode, setMode] = React.useState(url.searchParams.get('mode') || 'survey'); const [mode, setMode] = React.useState(url.searchParams.get('mode') || 'survey');
const [scrRequestPath, setScrRequestPath] = React.useState(''); const [scrRequestPath, setScrRequestPath] = React.useState('');
const clusters = getMemory().getClustersForOrigin(origin); const clusters = getMemory().getClustersForOrigin(origin || '');
React.useEffect(() => { React.useEffect(() => {
if (!origin) return;
const url = new URL(document.location.toString()); const url = new URL(document.location.toString());
url.searchParams.set('origin', origin); url.searchParams.set('origin', origin);
url.searchParams.set('answers', JSON.stringify(answers)); url.searchParams.set('answers', JSON.stringify(answers));
@ -38,8 +40,12 @@ function Report() {
history.pushState({}, 'Rentgen', url.toString()); history.pushState({}, 'Rentgen', url.toString());
}, [mode, answers, origin]); }, [mode, answers, origin]);
const visited_url = Object.values(clusters) const visited_url = Object.values(clusters)
.find((cluster) => cluster.getMarkedRequests().length > 0) .sort((clusterA, clusterB) => (clusterA.lastModified > clusterB.lastModified ? -1 : 1))
?.getMarkedRequests()[0].originalURL; .find((cluster) => !!cluster.lastFullUrl)?.lastFullUrl;
if (!visited_url) {
return <div>Wczytywanie...</div>;
}
const result = ( const result = (
<div {...{ 'data-version': counter }}> <div {...{ 'data-version': counter }}>

View File

@ -66,7 +66,7 @@ export default function ScreenshotGenerator({
}) { }) {
const [mode, setMode] = React.useState<string>('idle'); const [mode, setMode] = React.useState<string>('idle');
const [images, setImages] = React.useState<Screenshot[]>([]); const [images, setImages] = React.useState<Screenshot[]>([]);
const [taskId, setTaskId] = React.useState<string>(null); const [taskId, setTaskId] = React.useState<string | null>(null);
const [output, setOutput] = React.useState<any>({}); const [output, setOutput] = React.useState<any>({});
async function subscribeTask(path: string): Promise<screenshotTask> { async function subscribeTask(path: string): Promise<screenshotTask> {
@ -75,8 +75,8 @@ export default function ScreenshotGenerator({
await new Promise((resolve) => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
response = await (await pollTask(path)).json(); response = await (await pollTask(path)).json();
setImages((response as screenshotTask)?.images); setImages((response as screenshotTask)?.images);
document.querySelector('.images').scrollTo({ document.querySelector('.images')?.scrollTo({
top: document.querySelector('.images').scrollHeight, top: document.querySelector('.images')?.scrollHeight,
behavior: 'smooth', behavior: 'smooth',
}); });
} }

View File

@ -8,8 +8,8 @@ import verbs, { v } from './verbs';
export default function useSurvey( export default function useSurvey(
clusters: RequestCluster[], clusters: RequestCluster[],
{ onComplete }: { onComplete: (sender: { data: RawAnswers }) => void } { onComplete }: { onComplete: (sender: { data: RawAnswers }) => void }
): Survey.ReactSurveyModel { ): Survey.ReactSurveyModel | null {
const [survey, setSurvey] = React.useState<Survey.Model>(null); const [survey, setSurvey] = React.useState<Survey.Model | null>(null);
React.useEffect(() => { React.useEffect(() => {
const model = generateSurveyQuestions(clusters); const model = generateSurveyQuestions(clusters);
const survey = new Survey.Model(model); const survey = new Survey.Model(model);

View File

@ -1,20 +1,11 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Options from '../../options';
import { StolenData } from './stolen-data';
import { getshorthost, useEmitter } from '../../util';
import { getMemory } from '../../memory'; import { getMemory } from '../../memory';
import Options from '../../options';
async function getCurrentTab() { import { useEmitter } from '../../util';
const [tab] = await browser.tabs.query({
active: true,
windowId: browser.windows.WINDOW_ID_CURRENT,
});
return tab;
}
import './../../styles/global.scss'; import './../../styles/global.scss';
import './sidebar.scss'; import './sidebar.scss';
import { StolenData } from './stolen-data';
const Sidebar = () => { const Sidebar = () => {
const url = new URL(document.location.toString()); const url = new URL(document.location.toString());
@ -28,8 +19,8 @@ const Sidebar = () => {
const [cookiesOnly, setCookiesOnly] = React.useState<boolean>(false); const [cookiesOnly, setCookiesOnly] = React.useState<boolean>(false);
const [stolenDataView, setStolenDataView] = React.useState<boolean>(true); const [stolenDataView, setStolenDataView] = React.useState<boolean>(true);
const [cookiesOrOriginOnly, setCookiesOrOriginOnly] = React.useState<boolean>(false); const [cookiesOrOriginOnly, setCookiesOrOriginOnly] = React.useState<boolean>(false);
const [eventCounts, setEventCounts] = useEmitter(getMemory()); const [eventCounts] = useEmitter(getMemory());
const [marksOccurrence, setMarksOccurrence] = React.useState<boolean>(false); const [_, setMarksOccurrence] = React.useState<boolean>(false);
const [infoDataDialogAck, setInfoDataDialogAck] = React.useState<boolean>( const [infoDataDialogAck, setInfoDataDialogAck] = React.useState<boolean>(
localStorage.getItem('infoDataDialogAck') === null localStorage.getItem('infoDataDialogAck') === null
? true ? true
@ -53,6 +44,7 @@ const Sidebar = () => {
); );
React.useEffect(() => { React.useEffect(() => {
if (!origin) return;
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) { for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
if (cluster.hasMarks()) { if (cluster.hasMarks()) {
return setMarksOccurrence(true); return setMarksOccurrence(true);
@ -62,6 +54,7 @@ const Sidebar = () => {
return setMarksOccurrence(false); return setMarksOccurrence(false);
}, [eventCounts['*']]); }, [eventCounts['*']]);
if (!origin) return <div>Błąd: Brak parametru "origin"</div>;
return ( return (
<div className="sidebar"> <div className="sidebar">
<header className="header"> <header className="header">
@ -204,7 +197,7 @@ const Sidebar = () => {
<StolenData <StolenData
origin={origin} origin={origin}
eventCounts={eventCounts} eventCounts={eventCounts}
minValueLength={minValueLength} minValueLength={minValueLength === null ? 7 : minValueLength}
cookiesOnly={cookiesOnly} cookiesOnly={cookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly} cookiesOrOriginOnly={cookiesOrOriginOnly}
detailsVisibility={detailsVisibility} detailsVisibility={detailsVisibility}
@ -212,7 +205,7 @@ const Sidebar = () => {
</> </>
) : ( ) : (
<Options <Options
minValueLength={minValueLength} minValueLength={minValueLength === null ? 7 : minValueLength}
setMinValueLength={setMinValueLength} setMinValueLength={setMinValueLength}
cookiesOnly={cookiesOnly} cookiesOnly={cookiesOnly}
setCookiesOnly={setCookiesOnly} setCookiesOnly={setCookiesOnly}

View File

@ -14,7 +14,7 @@ export function StolenData({
detailsVisibility, detailsVisibility,
}: { }: {
origin: string; origin: string;
eventCounts: Record<string, number>; eventCounts: Record<string, number | undefined>;
minValueLength: number; minValueLength: number;
cookiesOnly: boolean; cookiesOnly: boolean;
cookiesOrOriginOnly: boolean; cookiesOrOriginOnly: boolean;
@ -43,7 +43,7 @@ export function StolenData({
origin={origin} origin={origin}
shorthost={cluster.id} shorthost={cluster.id}
key={cluster.id + origin} key={cluster.id + origin}
refreshToken={eventCounts[cluster.id]} refreshToken={eventCounts[cluster.id] || 0}
minValueLength={minValueLength} minValueLength={minValueLength}
cookiesOnly={cookiesOnly} cookiesOnly={cookiesOnly}
cookiesOrOriginOnly={cookiesOrOriginOnly} cookiesOrOriginOnly={cookiesOrOriginOnly}

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { Tab } from '../../util';
export default function TabDropdown({ export default function TabDropdown({
setPickedTab, setPickedTab,
@ -7,7 +8,7 @@ export default function TabDropdown({
setPickedTab: (tab_id: number) => void; setPickedTab: (tab_id: number) => void;
pickedTab: number; pickedTab: number;
}) { }) {
const [tabs, setTabs] = React.useState([]); const [tabs, setTabs] = React.useState<Tab[]>([]);
React.useEffect(() => { React.useEffect(() => {
browser.tabs.query({ currentWindow: true }).then(setTabs); browser.tabs.query({ currentWindow: true }).then(setTabs);
}, []); }, []);

View File

@ -16,10 +16,9 @@ import './toolbar.scss';
const Toolbar = () => { const Toolbar = () => {
const [origin, setOrigin] = React.useState<string | null>(null); const [origin, setOrigin] = React.useState<string | null>(null);
const [stolenDataView, setStolenDataView] = React.useState<boolean>(true); const [eventCounts] = useEmitter(getMemory());
const [eventCounts, setEventCounts] = useEmitter(getMemory());
const [cookieDomainCopy, setCookieDomainCopy] = React.useState<string | null>(null); const [cookieDomainCopy, setCookieDomainCopy] = React.useState<string | null>(null);
const [marksOccurrence, setMarksOccurrence] = React.useState<boolean>(false); const [_, setMarksOccurrence] = React.useState<boolean>(false);
const [exposedOriginDomainCopy, setExposedOriginDomainCopy] = React.useState<string | null>( const [exposedOriginDomainCopy, setExposedOriginDomainCopy] = React.useState<string | null>(
null null
); );
@ -33,6 +32,7 @@ const Toolbar = () => {
const tab = await getCurrentTab(); const tab = await getCurrentTab();
if (tab !== undefined) { if (tab !== undefined) {
if (!tab.url) return;
const url = new URL(tab.url); const url = new URL(tab.url);
if (url.origin.startsWith('moz-extension')) { if (url.origin.startsWith('moz-extension')) {
return; return;
@ -58,23 +58,27 @@ const Toolbar = () => {
switch (exposedOriginDomains.length) { switch (exposedOriginDomains.length) {
case 0: case 0:
return null; break;
case 1: case 1:
return setExposedOriginDomainCopy(`${exposedOriginDomains[0]}.`); setExposedOriginDomainCopy(`${exposedOriginDomains[0]}.`);
break;
case 2: case 2:
return setExposedOriginDomainCopy( setExposedOriginDomainCopy(
`${exposedOriginDomains[0]} oraz ${exposedOriginDomains[1]}.` `${exposedOriginDomains[0]} oraz ${exposedOriginDomains[1]}.`
); );
break;
case 3: case 3:
return setExposedOriginDomainCopy( setExposedOriginDomainCopy(
`${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} oraz ${exposedOriginDomains[2]}.` `${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} oraz ${exposedOriginDomains[2]}.`
); );
break;
default: default:
return setExposedOriginDomainCopy( setExposedOriginDomainCopy(
`${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} (i ${ `${exposedOriginDomains[0]}, ${exposedOriginDomains[1]} (i ${
exposedOriginDomains.length - 2 < 2 ? 2 : exposedOriginDomains.length - 2 exposedOriginDomains.length - 2 < 2 ? 2 : exposedOriginDomains.length - 2
} innych).` } innych).`
); );
break;
} }
}, [eventCounts['*'], origin]); }, [eventCounts['*'], origin]);
@ -86,25 +90,30 @@ const Toolbar = () => {
switch (cookieDomains.length) { switch (cookieDomains.length) {
case 0: case 0:
return null; null;
case 1: case 1:
return setCookieDomainCopy(`${cookieDomains[0]}.`); setCookieDomainCopy(`${cookieDomains[0]}.`);
break;
case 2: case 2:
return setCookieDomainCopy(`${cookieDomains[0]} oraz ${cookieDomains[1]}.`); setCookieDomainCopy(`${cookieDomains[0]} oraz ${cookieDomains[1]}.`);
break;
case 3: case 3:
return setCookieDomainCopy( setCookieDomainCopy(
`${cookieDomains[0]}, ${cookieDomains[1]} oraz ${cookieDomains[2]}.` `${cookieDomains[0]}, ${cookieDomains[1]} oraz ${cookieDomains[2]}.`
); );
break;
default: default:
return setCookieDomainCopy( setCookieDomainCopy(
`${cookieDomains[0]}, ${cookieDomains[1]} (i ${ `${cookieDomains[0]}, ${cookieDomains[1]} (i ${
cookieDomains.length - 2 < 2 ? 2 : cookieDomains.length - 2 cookieDomains.length - 2 < 2 ? 2 : cookieDomains.length - 2
} innych).` } innych).`
); );
break;
} }
}, [eventCounts['*'], origin]); }, [eventCounts['*'], origin]);
React.useEffect(() => { React.useEffect(() => {
if (!origin) return;
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) { for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
if (cluster.hasMarks()) { if (cluster.hasMarks()) {
return setMarksOccurrence(true); return setMarksOccurrence(true);
@ -115,12 +124,17 @@ const Toolbar = () => {
}, [eventCounts['*']]); }, [eventCounts['*']]);
function autoMark() { function autoMark() {
if (!origin) return;
for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) { for (const cluster of Object.values(getMemory().getClustersForOrigin(origin))) {
cluster.autoMark(); cluster.autoMark();
} }
return setMarksOccurrence(true); return setMarksOccurrence(true);
} }
if (!origin) {
return <div>Wczytywanie...</div>;
}
return ( return (
<div className="toolbar"> <div className="toolbar">
<header className="header"> <header className="header">

View File

@ -75,12 +75,12 @@ export default class ExtendedRequest {
public tabId: number; public tabId: number;
public url: string; public url: string;
public shorthost: string; public shorthost: string;
public requestHeaders: Request['requestHeaders'] = []; public requestHeaders: { name: string; value?: string; binaryValue?: number[] }[] = [];
public originalURL: string; public originalURL: string | null = null;
public origin: string; public origin: string;
public initialized = false; public initialized = false;
public stolenData: StolenDataEntry[]; public stolenData: StolenDataEntry[] = [];
public originalPathname: string; public originalPathname: string | null = null;
public requestBody: RequestBody; public requestBody: RequestBody;
static by_id = {} as Record<string, ExtendedRequest>; static by_id = {} as Record<string, ExtendedRequest>;
@ -97,20 +97,21 @@ export default class ExtendedRequest {
(this.data as any).frameAncestors = [ (this.data as any).frameAncestors = [
...(data as any).frameAncestors.map((e: any) => ({ url: e.url })), ...(data as any).frameAncestors.map((e: any) => ({ url: e.url })),
]; ];
this.origin = this.cacheOrigin();
} }
addHeaders(headers: Request['requestHeaders']) { addHeaders(headers: Request['requestHeaders']) {
this.requestHeaders = headers; this.requestHeaders = headers || [];
return this; return this;
} }
async init() { init() {
await this.cacheOrigin(); this.cacheOrigin();
this.initialized = true; this.initialized = true;
this.stolenData = this.getAllStolenData(); this.stolenData = this.getAllStolenData();
} }
async cacheOrigin(): Promise<void> { cacheOrigin(): string {
let url: string; let url: string;
if (this.data.type === 'main_frame') { if (this.data.type === 'main_frame') {
url = this.data.url; url = this.data.url;
@ -135,6 +136,7 @@ export default class ExtendedRequest {
this.originalURL = url; this.originalURL = url;
this.origin = new URL(url).origin; this.origin = new URL(url).origin;
this.originalPathname = new URL(url).pathname; this.originalPathname = new URL(url).pathname;
return this.origin;
} }
isThirdParty() { isThirdParty() {

View File

@ -1,4 +1,3 @@
import { EventEmitter } from 'events';
import ExtendedRequest from './extended-request'; import ExtendedRequest from './extended-request';
import { SaferEmitter } from './safer-emitter'; import { SaferEmitter } from './safer-emitter';
import { Sources, StolenDataEntry } from './stolen-data-entry'; import { Sources, StolenDataEntry } from './stolen-data-entry';
@ -11,12 +10,18 @@ export class RequestCluster extends SaferEmitter {
public requests: ExtendedRequest[] = []; public requests: ExtendedRequest[] = [];
public representativeStolenData: StolenDataEntry[] = []; public representativeStolenData: StolenDataEntry[] = [];
public expanded: boolean; public expanded: boolean;
public lastModified: number = 0;
public lastFullUrl: string | null = null;
constructor(public id: string) { constructor(public id: string) {
super(); super();
} }
add(request: ExtendedRequest) { add(request: ExtendedRequest) {
this.requests.push(request); this.requests.push(request);
this.emit('change'); this.emit('change');
this.lastModified = Date.now();
if (request.originalURL) {
this.lastFullUrl = request.originalURL;
}
} }
toggleExpanded(state: boolean) { toggleExpanded(state: boolean) {

View File

@ -8,6 +8,8 @@
"typeRoots": ["node_modules/@types", "node_modules/web-ext-types"], "typeRoots": ["node_modules/@types", "node_modules/web-ext-types"],
"target": "es2019", "target": "es2019",
"outDir": "lib", "outDir": "lib",
"skipLibCheck": true "skipLibCheck": true,
"strictNullChecks": true,
"strict": true
} }
} }

23
util.ts
View File

@ -36,7 +36,11 @@ export function getshorthost(host: string) {
.replace(/^.*:\/\//, '') .replace(/^.*:\/\//, '')
.replace(/\/.*$/, '') .replace(/\/.*$/, '')
.split('.'); .split('.');
let lookback = !['co', 'com'].includes(parts.at(-2)) ? -2 : -3; const second_last = parts.at(-2);
if (!second_last) {
throw new Error('url too short?');
}
let lookback = !['co', 'com'].includes(second_last) ? -2 : -3;
if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') { if (parts.at(-2) == 'doubleclick' || parts.at(-2) == 'google') {
lookback = -4; // to distinguish between google ads and stats lookback = -4; // to distinguish between google ads and stats
} else if (parts.at(-2) == 'google') { } else if (parts.at(-2) == 'google') {
@ -89,7 +93,7 @@ export async function getTabByID(id: number) {
} }
export function parseToObject(str: unknown): Record<string | symbol, unknown> { export function parseToObject(str: unknown): Record<string | symbol, unknown> {
let result: Record<string | symbol, unknown>; let result: Record<string | symbol, unknown> = {};
let original_string: string; let original_string: string;
if (typeof str === 'string') { if (typeof str === 'string') {
original_string = str; original_string = str;
@ -97,6 +101,8 @@ export function parseToObject(str: unknown): Record<string | symbol, unknown> {
} else if (typeof str == 'object') { } else if (typeof str == 'object') {
result = str as Record<string | symbol, unknown>; result = str as Record<string | symbol, unknown>;
original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str); original_string = (result[Symbol.for('originalString')] as string) || JSON.stringify(str);
} else {
return {};
} }
result[Symbol.for('originalString')] = original_string; result[Symbol.for('originalString')] = original_string;
return result; return result;
@ -149,10 +155,11 @@ export function getDate() {
} }
export function toBase64(file: File): Promise<string> { export function toBase64(file: File): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve, reject) => {
const FR = new FileReader(); const FR = new FileReader();
FR.addEventListener('load', (e) => { FR.addEventListener('load', (e) => {
resolve(e.target.result as string); const target = e.target;
target ? resolve(target.result as string) : reject('empty file?');
}); });
FR.readAsDataURL(file); FR.readAsDataURL(file);
}); });
@ -199,7 +206,8 @@ export function isBase64JSON(s: unknown): s is string {
export function flattenObject( export function flattenObject(
obj: unknown, obj: unknown,
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString(), parser: (to_parse: { toString: () => string }) => string | Record<string, unknown> = (id) =>
id.toString(),
key = '', key = '',
ret = [] as [string, string][], ret = [] as [string, string][],
parsed = false parsed = false
@ -220,7 +228,7 @@ export function flattenObject(
flattenObject(value, parser, prefix + subkey, ret); flattenObject(value, parser, prefix + subkey, ret);
} }
} else if (!parsed) { } else if (!parsed) {
flattenObject(parser(obj), parser, key, ret, true); flattenObject(parser(obj as { toString: () => string }), parser, key, ret, true);
} else if (typeof obj === 'string') { } else if (typeof obj === 'string') {
ret.push([key, obj]); ret.push([key, obj]);
} else { } else {
@ -231,7 +239,8 @@ export function flattenObject(
export function flattenObjectEntries( export function flattenObjectEntries(
entries: [string, unknown][], entries: [string, unknown][],
parser: (to_parse: unknown) => string | Record<string, unknown> = (id) => id.toString() parser: (to_parse: { toString: () => string }) => string | Record<string, unknown> = (id) =>
id.toString()
): [string, string][] { ): [string, string][] {
return flattenObject(Object.fromEntries(entries), parser); return flattenObject(Object.fromEntries(entries), parser);
} }