Skip to content

Commit c0e6b7d

Browse files
authored
Lyt 133: Pinning domain sometimes throws an uncaught cross-origin SecurityError (#134)
1 parent afad95e commit c0e6b7d

9 files changed

Lines changed: 130 additions & 50 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "lytics-dev-tools",
33
"version": "2.0.0",
4-
"title": "Contentstack Data & Insights Dev Tools",
4+
"title": "Lytics Dev Tools",
55
"description": "Advanced Chrome extension v2 for exploring, debugging, and enhancing the Lytics web SDK and APIs with powerful new features.",
66
"license": "MIT",
77
"repository": {

src/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
declare const __TAGLINK_BUILD_ID__: string;
2+
13
declare module 'virtual:reload-on-update-in-background-script' {
24
export const reloadOnUpdate: (watchPath: string) => void;
35
export default reloadOnUpdate;

src/pages/content/modules/scriptInjection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('Script Injection Module', () => {
4545
injectScript();
4646

4747
expect(document.createElement).toHaveBeenCalledWith('script');
48-
expect(mockScript.src).toBe('chrome-extension://test/tagLink.js');
48+
expect(mockScript.src).toMatch(/^chrome-extension:\/\/test\/tagLink\.js\?v=/);
4949
expect(document.documentElement.appendChild).toHaveBeenCalledWith(mockScript);
5050
});
5151

src/pages/content/modules/scriptInjection.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ export const injectScript = (scriptInjectedRef?: MutableRefObject<boolean>) => {
1010

1111
const script = document.createElement('script');
1212

13-
script.src = chrome.runtime.getURL('/tagLink.js');
13+
// Cache-bust: tagLink.js keeps a stable filename (required by manifest.web_accessible_resources),
14+
// so without a query param the browser can keep serving an old cached copy after an extension update.
15+
const buildId = typeof __TAGLINK_BUILD_ID__ !== 'undefined' ? __TAGLINK_BUILD_ID__ : '';
16+
script.src = `${chrome.runtime.getURL('/tagLink.js')}?v=${buildId}`;
1417
script.onload = () => {
1518
EmitLog({ name: 'content', payload: { msg: 'TagLink script loaded successfully' } });
1619
if (scriptInjectedRef) {

src/pages/sidepanel/sections/TagActivity.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const TagActivity = ({ textContent = appContent.tagActivity }: TagActivityProps)
9292
{tagActivity.map((item, index) => (
9393
<>
9494
{item && (
95-
<StyledAccordion disableGutters key={index}>
95+
<StyledAccordion disableGutters key={item.ts} TransitionProps={{ unmountOnExit: true }}>
9696
<AccordionSummary expandIcon={<ExpandMore />} aria-controls="panel1a-content" id={index.toString()}>
9797
<Stack
9898
direction={'row'}

src/pages/tagLink/tagLink.js

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ class TagLinkInternal {
7070
if (!this.jstagExist()) return;
7171

7272
window.jstag.call('entityReady', () => {
73-
this.dispatchEvent('config', window.jstag.config);
73+
try {
74+
this.dispatchEvent('config', window.jstag.config);
75+
} catch (err) {
76+
this.emitLog('tag', { msg: 'failed to read config', error: String(err) });
77+
}
7478
});
7579
}
7680

@@ -79,8 +83,12 @@ class TagLinkInternal {
7983

8084
window.jstag.call('entityReady', entity => {
8185
window.jstag.getid(id => {
82-
entity.data._uid = id;
83-
this.dispatchEvent('entity', entity);
86+
try {
87+
entity.data._uid = id;
88+
this.dispatchEvent('entity', entity);
89+
} catch (err) {
90+
this.emitLog('tag', { msg: 'failed to read entity', error: String(err) });
91+
}
8492
});
8593
});
8694
}
@@ -100,27 +108,54 @@ class TagLinkInternal {
100108
`);
101109
}
102110

111+
safeClone(value, seen = new WeakSet()) {
112+
if (value === null || typeof value !== 'object') {
113+
return typeof value === 'bigint' ? value.toString() : value;
114+
}
115+
116+
try {
117+
if (value === value.window || value.self === value) return undefined;
118+
} catch {
119+
return undefined;
120+
}
121+
122+
// Only walk plain data (object literals / arrays). Anything else - DOM nodes, Window,
123+
// class instances, Map/Set, etc. - can reach huge or exotic graphs (e.g. a DOM element's
124+
// ownerDocument/parentNode chain), so skip it rather than cloning it.
125+
const proto = Object.getPrototypeOf(value);
126+
const isPlainObject = proto === Object.prototype || proto === null;
127+
if (!Array.isArray(value) && !isPlainObject) {
128+
return undefined;
129+
}
130+
131+
if (seen.has(value)) return undefined;
132+
seen.add(value);
133+
134+
if (Array.isArray(value)) return value.map(v => this.safeClone(v, seen));
135+
136+
const out = {};
137+
for (const key in value) {
138+
try {
139+
const v = value[key];
140+
if (typeof v !== 'function') out[key] = this.safeClone(v, seen);
141+
} catch {
142+
// skip unreadable (cross-origin) property
143+
}
144+
}
145+
return out;
146+
}
147+
103148
dispatchEvent(name, payload) {
104-
const safeStringify = obj => {
105-
const seen = new WeakSet();
106-
107-
return JSON.stringify(obj, (key, value) => {
108-
if (value !== null && typeof value === 'object') {
109-
if (seen.has(value)) {
110-
return undefined;
111-
}
112-
seen.add(value);
113-
}
114-
return value;
149+
try {
150+
const customEvent = new CustomEvent(name, {
151+
detail: {
152+
data: JSON.stringify(this.safeClone(payload)),
153+
},
115154
});
116-
};
117-
118-
const customEvent = new CustomEvent(name, {
119-
detail: {
120-
data: safeStringify(payload),
121-
},
122-
});
123-
document.dispatchEvent(customEvent);
155+
document.dispatchEvent(customEvent);
156+
} catch (err) {
157+
this.emitLog('tag', { msg: 'failed to serialize payload', name, error: String(err) });
158+
}
124159
}
125160
}
126161

src/pages/tagLink/tagLink.tsx

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ class TagLinkInternal {
6969
if (!this.jstagExist()) return;
7070

7171
(window as any).jstag.call('entityReady', () => {
72-
this.dispatchEvent('config', (window as any).jstag.config);
72+
try {
73+
this.dispatchEvent('config', (window as any).jstag.config);
74+
} catch (err) {
75+
this.emitLog('tag', { msg: 'failed to read config', error: String(err) });
76+
}
7377
});
7478
}
7579

@@ -78,9 +82,12 @@ class TagLinkInternal {
7882

7983
(window as any).jstag.call('entityReady', entity => {
8084
(window as any).jstag.getid(id => {
81-
// this.emitLog('tag', 'got entity');
82-
entity.data._uid = id;
83-
this.dispatchEvent('entity', entity);
85+
try {
86+
entity.data._uid = id;
87+
this.dispatchEvent('entity', entity);
88+
} catch (err) {
89+
this.emitLog('tag', { msg: 'failed to read entity', error: String(err) });
90+
}
8491
});
8592
});
8693
}
@@ -100,27 +107,54 @@ class TagLinkInternal {
100107
`);
101108
}
102109

110+
safeClone(value: any, seen = new WeakSet()): any {
111+
if (value === null || typeof value !== 'object') {
112+
return typeof value === 'bigint' ? value.toString() : value;
113+
}
114+
115+
try {
116+
if (value === value.window || value.self === value) return undefined;
117+
} catch {
118+
return undefined;
119+
}
120+
121+
// Only walk plain data (object literals / arrays). Anything else - DOM nodes, Window,
122+
// class instances, Map/Set, etc. - can reach huge or exotic graphs (e.g. a DOM element's
123+
// ownerDocument/parentNode chain), so skip it rather than cloning it.
124+
const proto = Object.getPrototypeOf(value);
125+
const isPlainObject = proto === Object.prototype || proto === null;
126+
if (!Array.isArray(value) && !isPlainObject) {
127+
return undefined;
128+
}
129+
130+
if (seen.has(value)) return undefined;
131+
seen.add(value);
132+
133+
if (Array.isArray(value)) return value.map(v => this.safeClone(v, seen));
134+
135+
const out: Record<string, any> = {};
136+
for (const key in value) {
137+
try {
138+
const v = value[key];
139+
if (typeof v !== 'function') out[key] = this.safeClone(v, seen);
140+
} catch {
141+
// skip unreadable (cross-origin) property
142+
}
143+
}
144+
return out;
145+
}
146+
103147
dispatchEvent(name: string, payload: any) {
104-
const safeStringify = (obj: any) => {
105-
const seen = new WeakSet();
106-
107-
return JSON.stringify(obj, (key, value) => {
108-
if (value !== null && typeof value === 'object') {
109-
if (seen.has(value)) {
110-
return undefined;
111-
}
112-
seen.add(value);
113-
}
114-
return value;
148+
try {
149+
const customEvent = new CustomEvent(name, {
150+
detail: {
151+
data: JSON.stringify(this.safeClone(payload)),
152+
},
115153
});
116-
};
117-
118-
const customEvent = new CustomEvent(name, {
119-
detail: {
120-
data: safeStringify(payload),
121-
},
122-
});
123-
document.dispatchEvent(customEvent);
154+
document.dispatchEvent(customEvent);
155+
} catch (err) {
156+
this.emitLog('tag', { msg: 'failed to serialize payload', name, error: String(err) });
157+
}
124158
}
125159
}
126160

src/shared/storages/tabStateStorage.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EmitLog } from '@src/shared/components/EmitLog';
33
import { EventModel } from '@src/shared/models/eventModel';
44

55
const TabStateStorageKey = 'domainStates';
6+
const MAX_TAG_ACTIVITY_EVENTS = 200;
67

78
export interface DomainState {
89
domain: string;
@@ -129,7 +130,9 @@ const domainStateStore: DomainStateStorage = {
129130
allStates[domain] = createDefaultDomainState(domain);
130131
}
131132

132-
allStates[domain].tagActivity = [...(allStates[domain].tagActivity || []), event];
133+
const nextActivity = [...(allStates[domain].tagActivity || []), event];
134+
allStates[domain].tagActivity =
135+
nextActivity.length > MAX_TAG_ACTIVITY_EVENTS ? nextActivity.slice(-MAX_TAG_ACTIVITY_EVENTS) : nextActivity;
133136
allStates[domain].lastUpdated = Date.now();
134137

135138
await storage.set(allStates);

vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ const assetsDir = resolve(srcDir, 'assets');
1717
const publicDir = resolve(rootDir, 'public');
1818

1919
export default defineConfig({
20+
define: {
21+
__TAGLINK_BUILD_ID__: JSON.stringify(Date.now()),
22+
},
2023
resolve: {
2124
alias: {
2225
'@root': rootDir,

0 commit comments

Comments
 (0)