octocode-cli 1.3.1 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -34
- package/out/chunks/chunk-7476PETK.js +309 -0
- package/out/chunks/chunk-CVNNNSMQ.js +26 -0
- package/out/chunks/chunk-OQBJTZWK.js +60 -0
- package/out/chunks/chunk-UCZCF3BQ.js +9 -0
- package/out/chunks/command-help-specs-JZXVSLZ5.js +8 -0
- package/out/chunks/commands-XBFPLHSQ.js +8 -0
- package/out/chunks/help-P7TCOYAJ.js +10 -0
- package/out/chunks/main-help-ULF5PAQY.js +10 -0
- package/out/chunks/prompts-5E6VKRX5.js +8 -0
- package/out/chunks/spinner-URV2OX6O.js +8 -0
- package/out/chunks/tool-command-M6VA7P2F.js +8 -0
- package/out/octocode-cli.js +1 -1
- package/package.json +5 -3
- package/skills/README.md +13 -1
- package/skills/agentic-flow-best-practices/SKILL.md +280 -0
- package/skills/agentic-flow-best-practices/references/agent-collaboration-patterns.md +75 -0
- package/skills/agentic-flow-best-practices/references/pr-review-agent-example.md +47 -0
- package/skills/agentic-flow-best-practices/references/resources.md +112 -0
- package/skills/octocode-chrome-devtools/README.md +541 -0
- package/skills/octocode-chrome-devtools/SKILL.md +197 -0
- package/skills/octocode-chrome-devtools/agents/openai.yaml +7 -0
- package/skills/octocode-chrome-devtools/references/CDP_AGENT_REFERENCE.md +401 -0
- package/skills/octocode-chrome-devtools/references/CHROME_FLAGS.md +234 -0
- package/skills/octocode-chrome-devtools/references/INTENTS.md +108 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_AUTH.md +179 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_AUTOMATION.md +214 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_DEBUG.md +329 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_ENVIRONMENT.md +237 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_INSPECT.md +263 -0
- package/skills/octocode-chrome-devtools/references/INTENTS_STORAGE_CONSENT.md +214 -0
- package/skills/octocode-chrome-devtools/references/RECOVERY.md +39 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS.md +43 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_ASYNC_WORKERS.md +345 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_BROWSER.md +403 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_OBSERVE.md +275 -0
- package/skills/octocode-chrome-devtools/references/SCRIPT_PATTERNS_SPECIAL.md +18 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-runner.mjs +503 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-sandbox.mjs +123 -0
- package/skills/octocode-chrome-devtools/scripts/cdp-template.mjs +81 -0
- package/skills/octocode-chrome-devtools/scripts/octocode-chrome-devtools.vpn.example.json +8 -0
- package/skills/octocode-chrome-devtools/scripts/open-browser.mjs +362 -0
- package/skills/octocode-chrome-devtools/scripts/sourcemap-resolver.mjs +193 -0
- package/skills/octocode-chrome-devtools/scripts/undercover.mjs +226 -0
- package/skills/octocode-design/README.md +2 -2
- package/skills/octocode-documentation-writer/README.md +1 -1
- package/skills/octocode-engineer/README.md +1 -1
- package/skills/octocode-engineer/SKILL.md +4 -2
- package/skills/octocode-engineer/references/output-files.md +3 -3
- package/skills/octocode-engineer/scripts/run.js +136 -136
- package/skills/octocode-engineer/src/reporting/summary-md.test.ts +48 -0
- package/skills/octocode-engineer/src/reporting/summary-md.ts +43 -2
- package/skills/octocode-install/SKILL.md +1 -1
- package/skills/octocode-pull-request-reviewer/README.md +5 -5
- package/skills/octocode-pull-request-reviewer/SKILL.md +1 -1
- package/skills/octocode-research/AGENTS.md +1 -1
- package/skills/octocode-research/README.md +2 -2
- package/skills/octocode-research/docs/ARCHITECTURE.md +1 -1
- package/skills/octocode-research/scripts/server.js +191 -246
- package/skills/octocode-research/src/routes/github.ts +4 -21
- package/skills/octocode-research/src/routes/local.ts +4 -21
- package/skills/octocode-research/src/utils/fileContentTransform.ts +44 -0
- package/skills/octocode-search-skill/SKILL.md +245 -229
- package/skills/octocode-search-skill/references/agent-skills-guide.md +177 -0
- package/skills/octocode-search-skill/references/discovery-surfaces.md +162 -0
- package/skills/octocode-search-skill/references/fetch-and-create-locally.md +57 -0
- package/skills/octocode-search-skill/references/install-reference.md +130 -0
- package/skills/octocode-search-skill/references/references-template.md +27 -0
- package/skills/octocode-search-skill/references/references.md +62 -0
- package/skills/octocode-slides/README.md +307 -0
- package/skills/octocode-slides/SKILL.md +410 -0
- package/skills/octocode-slides/references/01-brief.md +156 -0
- package/skills/octocode-slides/references/02-research.md +149 -0
- package/skills/octocode-slides/references/03-outline.md +172 -0
- package/skills/octocode-slides/references/04-design.md +301 -0
- package/skills/octocode-slides/references/05-implementation.md +213 -0
- package/skills/octocode-slides/references/06-review.md +258 -0
- package/skills/octocode-slides/references/animation.md +281 -0
- package/skills/octocode-slides/references/design-system.md +316 -0
- package/skills/octocode-slides/references/html-templates.md +673 -0
- package/skills/octocode-slides/references/image-generation.md +448 -0
- package/skills/octocode-slides/references/resources.md +840 -0
- package/skills/octocode-slides/references/slide-rules.md +541 -0
- package/skills/octocode-slides/references/wireframes.md +727 -0
- package/skills/octocode-slides/scripts/animation.js +182 -0
- package/skills/octocode-slides/scripts/base.css +353 -0
- package/skills/octocode-slides/scripts/base.html +655 -0
- package/skills/octocode-slides/scripts/generate_image.py +221 -0
- package/skills/octocode-slides/scripts/navbridge.js +79 -0
- package/skills/octocode-slides/scripts/presenter.js +316 -0
- package/skills/octocode-slides/scripts/slide.html +248 -0
- package/skills/octocode-stats/SKILL.md +73 -0
- package/skills/octocode-stats/assets/template.html +1332 -0
- package/skills/octocode-stats/scripts/build_dashboard.mjs +407 -0
- package/assets/example.png +0 -0
- package/out/chunks/chunk-QCY7Q7YW.js +0 -389
- package/out/chunks/command-help-specs-CQ3RBLP6.js +0 -8
- package/out/chunks/commands-OCTZP2TO.js +0 -51
- package/out/chunks/help-XPXP46ZT.js +0 -10
- package/out/chunks/main-help-35HX2UDQ.js +0 -10
- package/out/chunks/tool-command-HOSMVLNK.js +0 -8
- package/skills/octocode-search-skill/INSTALL_REFERENCE.md +0 -112
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# CDP Storage And Consent Intent Details
|
|
2
|
+
|
|
3
|
+
## storage
|
|
4
|
+
|
|
5
|
+
**Trigger phrases:** "storage", "localStorage", "sessionStorage", "IndexedDB", "cookies", "cache storage", "service worker cache", "check what's stored", "what data does this site store", "storage quota", "offline data", "browser storage"
|
|
6
|
+
|
|
7
|
+
**Purpose:** Full metadata inventory of client-side storage — cookie names/flags, localStorage and sessionStorage keys/sizes, IndexedDB database names, Cache Storage caches, Service Worker registrations, and storage quota. Use for privacy audits, debugging persistence bugs, and session forensics without emitting stored secret values.
|
|
8
|
+
|
|
9
|
+
**Domains:** `Network.enable`, `Runtime.enable`, `Page.enable`
|
|
10
|
+
|
|
11
|
+
**Key events/methods:**
|
|
12
|
+
- `Network.getAllCookies` → all cookies across all domains in the browser jar (richer than `getCookies`)
|
|
13
|
+
- `Runtime.evaluate` → localStorage, sessionStorage, indexedDB.databases(), caches.keys(), navigator.serviceWorker.getRegistrations(), navigator.storage.estimate()
|
|
14
|
+
- `Network.getCookies({ urls })` → scoped to a specific origin when full jar is too noisy
|
|
15
|
+
|
|
16
|
+
**Script skeleton:**
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
export async function run(cdp) {
|
|
20
|
+
await cdp.send('Network.enable', {});
|
|
21
|
+
await cdp.send('Runtime.enable', {});
|
|
22
|
+
await cdp.send('Page.enable', {});
|
|
23
|
+
cdp.on('Page.javascriptDialogOpening', () =>
|
|
24
|
+
cdp.send('Page.handleJavaScriptDialog', { accept: false }).catch(() => {}));
|
|
25
|
+
|
|
26
|
+
await cdp.send('Page.navigate', { url: 'https://TARGET_URL/' });
|
|
27
|
+
await new Promise(r => setTimeout(r, 5000)); // settle
|
|
28
|
+
|
|
29
|
+
// ── Cookies (all domains) ─────────────────────────────────────────────────
|
|
30
|
+
const { cookies } = await cdp.send('Network.getAllCookies', {});
|
|
31
|
+
console.log(`[SECURITY] Cookies total: ${cookies.length}`);
|
|
32
|
+
for (const c of cookies) {
|
|
33
|
+
const flags = [c.httpOnly ? 'httpOnly' : 'NO_httpOnly', c.secure ? 'secure' : 'NO_secure', `sameSite=${c.sameSite || 'None'}`].join(' ');
|
|
34
|
+
console.log(`[SECURITY] Cookie: ${c.name.padEnd(28)} domain=${c.domain} [${flags}]`);
|
|
35
|
+
if (!c.httpOnly) console.log(`[FINDING] COOKIE_NO_HTTPONLY: ${c.name}`);
|
|
36
|
+
if (!c.secure) console.log(`[FINDING] COOKIE_NO_SECURE: ${c.name}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── localStorage ─────────────────────────────────────────────────────────
|
|
40
|
+
const { result: lsR } = await cdp.send('Runtime.evaluate', {
|
|
41
|
+
expression: `JSON.stringify(Object.entries(localStorage).map(([k,v])=>({k,size:new Blob([k+v]).size,sensitive:/token|auth|jwt|secret|password|credential/i.test(k)})))`,
|
|
42
|
+
returnByValue: true,
|
|
43
|
+
});
|
|
44
|
+
const ls = JSON.parse(lsR.value || '[]');
|
|
45
|
+
console.log(`[STORAGE] localStorage: ${ls.length} keys, ~${ls.reduce((s,i)=>s+i.size,0)}B`);
|
|
46
|
+
for (const { k, size, sensitive } of ls) {
|
|
47
|
+
console.log(`[STORAGE] LS[${k}] size=${size}B`);
|
|
48
|
+
if (sensitive) console.log(`[FINDING] SENSITIVE_IN_STORAGE: localStorage["${k}"]`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── sessionStorage ────────────────────────────────────────────────────────
|
|
52
|
+
const { result: ssR } = await cdp.send('Runtime.evaluate', {
|
|
53
|
+
expression: `JSON.stringify(Object.entries(sessionStorage).map(([k,v])=>({k,size:new Blob([k+v]).size,sensitive:/token|auth|jwt|secret|password|credential/i.test(k)})))`,
|
|
54
|
+
returnByValue: true,
|
|
55
|
+
});
|
|
56
|
+
const ss = JSON.parse(ssR.value || '[]');
|
|
57
|
+
console.log(`[STORAGE] sessionStorage: ${ss.length} keys`);
|
|
58
|
+
for (const { k, size, sensitive } of ss) {
|
|
59
|
+
console.log(`[STORAGE] SS[${k}] size=${size}B`);
|
|
60
|
+
if (sensitive) console.log(`[FINDING] SENSITIVE_IN_STORAGE: sessionStorage["${k}"]`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── IndexedDB ─────────────────────────────────────────────────────────────
|
|
64
|
+
const { result: idbR } = await cdp.send('Runtime.evaluate', {
|
|
65
|
+
expression: `indexedDB.databases().then(dbs=>JSON.stringify(dbs))`,
|
|
66
|
+
returnByValue: true, awaitPromise: true,
|
|
67
|
+
});
|
|
68
|
+
const dbs = JSON.parse(idbR.value || '[]');
|
|
69
|
+
console.log(`[STORAGE] IndexedDB: ${dbs.length} databases`);
|
|
70
|
+
for (const db of dbs) console.log(`[STORAGE] IDB: ${db.name} v${db.version}`);
|
|
71
|
+
|
|
72
|
+
// ── Service Workers ───────────────────────────────────────────────────────
|
|
73
|
+
const { result: swR } = await cdp.send('Runtime.evaluate', {
|
|
74
|
+
expression: `navigator.serviceWorker.getRegistrations().then(r=>JSON.stringify(r.map(s=>({scope:s.scope,state:s.active?.state??'none',script:s.active?.scriptURL}))))`,
|
|
75
|
+
returnByValue: true, awaitPromise: true,
|
|
76
|
+
});
|
|
77
|
+
const sws = JSON.parse(swR.value || '[]');
|
|
78
|
+
console.log(`[STORAGE] Service Workers: ${sws.length}`);
|
|
79
|
+
for (const sw of sws) console.log(`[STORAGE] SW: ${sw.scope} state=${sw.state} script=${sw.script}`);
|
|
80
|
+
|
|
81
|
+
// ── Cache Storage ─────────────────────────────────────────────────────────
|
|
82
|
+
const { result: cacheR } = await cdp.send('Runtime.evaluate', {
|
|
83
|
+
expression: `caches.keys().then(names=>Promise.all(names.map(n=>caches.open(n).then(c=>c.keys().then(k=>({name:n,count:k.length})))))).then(JSON.stringify)`,
|
|
84
|
+
returnByValue: true, awaitPromise: true,
|
|
85
|
+
});
|
|
86
|
+
const cachesData = JSON.parse(cacheR.value || '[]');
|
|
87
|
+
console.log(`[STORAGE] Cache Storage: ${cachesData.length} caches`);
|
|
88
|
+
for (const c of cachesData) console.log(`[STORAGE] Cache "${c.name}": ${c.count} entries`);
|
|
89
|
+
|
|
90
|
+
// ── Quota ─────────────────────────────────────────────────────────────────
|
|
91
|
+
const { result: quotaR } = await cdp.send('Runtime.evaluate', {
|
|
92
|
+
expression: `navigator.storage.estimate().then(e=>JSON.stringify({usedKB:Math.round(e.usage/1024),quotaMB:Math.round(e.quota/1024/1024),pct:((e.usage/e.quota)*100).toFixed(2)+'%'}))`,
|
|
93
|
+
returnByValue: true, awaitPromise: true,
|
|
94
|
+
});
|
|
95
|
+
console.log(`[STORAGE] Quota: ${quotaR.value}`);
|
|
96
|
+
|
|
97
|
+
// ── Cookie resurrection detection ─────────────────────────────────────────
|
|
98
|
+
// Checks whether tracking IDs exist in both cookies AND localStorage (persistence pattern)
|
|
99
|
+
const cookieNames = cookies.map(c => c.name);
|
|
100
|
+
for (const { k } of ls) {
|
|
101
|
+
if (cookieNames.includes(k))
|
|
102
|
+
console.log(`[FINDING] COOKIE_RESURRECTION: "${k}" duplicated in both cookies and localStorage — tracking persistence pattern`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Output prefixes:** `[STORAGE]` `[SECURITY]` `[FINDING]`
|
|
108
|
+
|
|
109
|
+
**Key findings to watch:**
|
|
110
|
+
- `COOKIE_NO_HTTPONLY` / `COOKIE_NO_SECURE` — tracking cookies readable by JS or transmittable over HTTP
|
|
111
|
+
- `SENSITIVE_IN_STORAGE` — token/auth/JWT keys in localStorage
|
|
112
|
+
- `COOKIE_RESURRECTION` — IDs stored in both cookies and localStorage (e.g. cross-domain tracker IDs)
|
|
113
|
+
- Large IndexedDB stores or Cache Storage entries indicate PWA / offline capability
|
|
114
|
+
|
|
115
|
+
**Combine with:** `security` (CSP, headers, POST body scan), `consent` (GDPR pre-grant check), `supply-chain` (third-party domain inventory).
|
|
116
|
+
|
|
117
|
+
## consent
|
|
118
|
+
|
|
119
|
+
**Trigger phrases:** "GDPR", "consent", "tracking", "privacy", "CMP", "Usercentrics", "OneTrust", "Cookiebot", "cookie banner", "ad consent", "is consent required", "what is tracked", "analytics opt-out", "adStorage", "pre-granted"
|
|
120
|
+
|
|
121
|
+
**Purpose:** Audit whether a Consent Management Platform (CMP) is present, whether consent is properly gated before trackers fire, and whether stored consent state matches what was granted. Works across Usercentrics, OneTrust, CookieYes, Cookiebot, TrustArc, and custom CMPs.
|
|
122
|
+
|
|
123
|
+
**Domains:** `Network.enable`, `Runtime.enable`, `Page.enable`
|
|
124
|
+
|
|
125
|
+
**Key signals:**
|
|
126
|
+
- Analytics and ad trackers firing before consent dialog appears → violation
|
|
127
|
+
- `ucData` / `_uetsid` / `_gcl_au` in localStorage with consent already granted → pre-grant
|
|
128
|
+
- `window.dataLayer` populated before consent → GTM firing too early
|
|
129
|
+
- `window.UC_UI` / `window.__tcfapi` / `window.Optanon` → CMP present
|
|
130
|
+
- Consent string in localStorage (`ucString` = Usercentrics, `eupubconsent-v2` = IAB TCF v2)
|
|
131
|
+
|
|
132
|
+
**Script skeleton:**
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
export async function run(cdp) {
|
|
136
|
+
await cdp.send('Network.enable', {});
|
|
137
|
+
await cdp.send('Runtime.enable', {});
|
|
138
|
+
await cdp.send('Page.enable', {});
|
|
139
|
+
cdp.on('Page.javascriptDialogOpening', () =>
|
|
140
|
+
cdp.send('Page.handleJavaScriptDialog', { accept: false }).catch(() => {}));
|
|
141
|
+
|
|
142
|
+
// Track tracker requests BEFORE consent can fire
|
|
143
|
+
const trackerHits = [];
|
|
144
|
+
cdp.on('Network.requestWillBeSent', ({ request, timestamp }) => {
|
|
145
|
+
const url = request.url;
|
|
146
|
+
if (/googletagmanager|google-analytics|clarity\.ms|bat\.bing|fbq|meta\.net|doubleclick|twitter|ads-twitter/i.test(url))
|
|
147
|
+
trackerHits.push({ url: url.substring(0, 100), ts: timestamp });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await cdp.send('Page.navigate', { url: 'https://TARGET_URL/' });
|
|
151
|
+
await new Promise(r => setTimeout(r, 6000));
|
|
152
|
+
|
|
153
|
+
// ── CMP detection ─────────────────────────────────────────────────────────
|
|
154
|
+
const { result: cmpR } = await cdp.send('Runtime.evaluate', {
|
|
155
|
+
expression: `JSON.stringify({
|
|
156
|
+
usercentrics: !!window.UC_UI || !!window.usercentrics,
|
|
157
|
+
onetrust: !!window.OneTrust || !!window.Optanon,
|
|
158
|
+
cookiebot: !!window.CookieConsent || !!window.Cookiebot,
|
|
159
|
+
trustArc: !!window.truste,
|
|
160
|
+
iabTCF: typeof window.__tcfapi === 'function',
|
|
161
|
+
gtmLoaded: Array.isArray(window.dataLayer),
|
|
162
|
+
dlEvents: (window.dataLayer || []).length,
|
|
163
|
+
})`,
|
|
164
|
+
returnByValue: true,
|
|
165
|
+
});
|
|
166
|
+
const cmp = JSON.parse(cmpR.value || '{}');
|
|
167
|
+
console.log(`[SECURITY] CMP detected: ${JSON.stringify(cmp)}`);
|
|
168
|
+
if (!Object.values(cmp).slice(0,5).some(Boolean)) console.log('[FINDING] NO_CMP: no consent management platform detected');
|
|
169
|
+
|
|
170
|
+
// ── Consent state in storage ───────────────────────────────────────────────
|
|
171
|
+
const { result: csR } = await cdp.send('Runtime.evaluate', {
|
|
172
|
+
expression: `JSON.stringify({
|
|
173
|
+
ucGcm: (() => { try { return JSON.parse(localStorage.getItem('ucData') || '{}').gcm || null; } catch { return null; } })(),
|
|
174
|
+
ucString: !!localStorage.getItem('ucString'),
|
|
175
|
+
iabConsent: !!localStorage.getItem('eupubconsent-v2'),
|
|
176
|
+
otConsent: !!localStorage.getItem('OptanonConsent'),
|
|
177
|
+
})`,
|
|
178
|
+
returnByValue: true,
|
|
179
|
+
});
|
|
180
|
+
const cs = JSON.parse(csR.value || '{}');
|
|
181
|
+
if (cs.ucGcm) {
|
|
182
|
+
const gcm = cs.ucGcm;
|
|
183
|
+
console.log(`[SECURITY] Usercentrics GCM flags: ${JSON.stringify(gcm)}`);
|
|
184
|
+
if (gcm.adStorage === 'granted') console.log('[FINDING] CONSENT_PRE_GRANTED: adStorage=granted without user interaction');
|
|
185
|
+
if (gcm.adPersonalization === 'granted') console.log('[FINDING] CONSENT_PRE_GRANTED: adPersonalization=granted');
|
|
186
|
+
if (gcm.analyticsStorage === 'granted') console.log('[FINDING] CONSENT_PRE_GRANTED: analyticsStorage=granted');
|
|
187
|
+
}
|
|
188
|
+
if (cs.iabConsent) console.log('[SECURITY] IAB TCF v2 string present (value redacted)');
|
|
189
|
+
if (cs.otConsent) console.log('[SECURITY] OneTrust consent string present (value redacted)');
|
|
190
|
+
|
|
191
|
+
// ── Trackers that fired before any consent interaction ─────────────────────
|
|
192
|
+
console.log(`[SECURITY] Tracker requests on cold load: ${trackerHits.length}`);
|
|
193
|
+
for (const h of trackerHits) console.log(`[SECURITY] Tracker fired: ${h.url}`);
|
|
194
|
+
if (trackerHits.length > 0) console.log(`[FINDING] TRACKERS_BEFORE_CONSENT: ${trackerHits.length} tracker requests fired before consent could be shown`);
|
|
195
|
+
|
|
196
|
+
// ── dataLayer events (GTM) ────────────────────────────────────────────────
|
|
197
|
+
const { result: dlR } = await cdp.send('Runtime.evaluate', {
|
|
198
|
+
expression: `JSON.stringify((window.dataLayer||[]).slice(0,10).map(e=>e.event||JSON.stringify(e).slice(0,60)))`,
|
|
199
|
+
returnByValue: true,
|
|
200
|
+
});
|
|
201
|
+
const dlEvents = JSON.parse(dlR.value || '[]');
|
|
202
|
+
console.log(`[SECURITY] dataLayer events: ${dlEvents.join(', ')}`);
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Output prefixes:** `[SECURITY]` `[FINDING]`
|
|
207
|
+
|
|
208
|
+
**Key findings to watch:**
|
|
209
|
+
- `NO_CMP` — no consent platform detected; all tracking is ungated
|
|
210
|
+
- `CONSENT_PRE_GRANTED` — adStorage/adPersonalization/analyticsStorage pre-granted (GDPR risk in EU)
|
|
211
|
+
- `TRACKERS_BEFORE_CONSENT` — trackers fire on first load before banner can appear
|
|
212
|
+
- Large `dataLayer` event count suggests aggressive GTM firing
|
|
213
|
+
|
|
214
|
+
**Combine with:** `storage` (full cookie + localStorage inventory), `supply-chain` (enumerate all third-party domains loaded), `network` (capture timing of tracker requests relative to page load).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Recovery Reference
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
| Situation | Fix |
|
|
5
|
+
|-----------|-----|
|
|
6
|
+
| `Chrome not found` | Install Chrome or check path in `open-browser.mjs` |
|
|
7
|
+
| `Chrome not running on port` | Run `open-browser.mjs --headless` first |
|
|
8
|
+
| Chrome already open, no CDP | Handled automatically — `open-browser.mjs` launches isolated CDP session |
|
|
9
|
+
| `WebSocket unavailable` | Upgrade to Node.js 22+ (native WebSocket required, no install needed) |
|
|
10
|
+
| `Script not found` | Use `$TMPDIR/cdp-<task>.mjs`, never hardcode `/tmp/` |
|
|
11
|
+
| `CDP timeout for <method>` | Domain not enabled — add the required `enable` call before using it |
|
|
12
|
+
| `No page targets found` | Use `--new-tab about:blank` to open a fresh tab |
|
|
13
|
+
| Need to inspect an iframe or service worker | Use `--list-targets` to discover, then `--target-url <pattern>` or `--target-type service_worker` |
|
|
14
|
+
| `[CDP_RETRY_NEEDED]` in output (exit 2) | Read the `[CDP_RETRY_NEEDED]` lines — fix the domain enable or method name, retry once |
|
|
15
|
+
| Bot-wall detected (bot-wall / CDN challenge instead of real page) | Pass `--userAgent` explicitly with a current Chrome desktop UA string only when needed. For sites that fingerprint JS (canvas, WebGL, timing): use the `user-auth` flow — visible browser, user solves the challenge in the CDP-controlled session. |
|
|
16
|
+
| `ERR_ACCESS_DENIED` in sandbox | Script tried to write outside `cdp.outputDir`, read a blocked path, or spawn a child process / Worker. Fix: all file writes via `join(cdp.outputDir, filename)`; all browser interaction via `cdp.send()`; no `child_process`, `net`, or `new Worker()`. |
|
|
17
|
+
| `[AUTH_TIMEOUT]` — user-auth script timed out | User did not authenticate within `TIMEOUT_MS`. Increase the timeout, verify `POST_AUTH_PATTERN` matches the actual post-login URL fragment, or set `AUTH_COOKIE_NAME` to the exact cookie the app sets on successful login. |
|
|
18
|
+
| Events not firing | Page loaded before listeners attached — attach listeners first, then call `Page.navigate` inside `run()` |
|
|
19
|
+
| `--new-tab <url>` misses network/script events | Tab loads before script connects — use `--new-tab about:blank` + `Page.navigate` inside `run()` |
|
|
20
|
+
| JavaScript dialog blocking all commands | Add dialog guard before navigate: `cdp.on('Page.javascriptDialogOpening', () => cdp.send('Page.handleJavaScriptDialog', { accept: true }))` — see Dialog guard in `CDP_AGENT_REFERENCE.md` section 0 |
|
|
21
|
+
| URL with `?` or `&` fails in zsh | Always quote the URL: `--url "http://..."` |
|
|
22
|
+
| `Runtime.evaluate` hangs after `Debugger.enable` | Add `await cdp.send('Debugger.setSkipAllPauses', { skip: true })` immediately after `Debugger.enable` |
|
|
23
|
+
| `Page.navigate` times out on ALL URLs | Chrome session is stale — run `open-browser.mjs --cleanup` then relaunch with `--headless` |
|
|
24
|
+
| Unsure whether cleanup will kill the tracked browser | Run `open-browser.mjs --cleanup --dry-run`; it reports whether the tracked PID matches both the CDP port and temp profile without killing anything |
|
|
25
|
+
| `Security.getSecurityState` not found | Removed from current CDP — use `Security.visibleSecurityStateChanged` event instead |
|
|
26
|
+
| `Storage.enable` not found (exit 2) | Not available in Chrome CDP (Chrome 120+). Remove the call — cookies, localStorage, sessionStorage, and IndexedDB are accessible without it via `Network.getAllCookies`, `Runtime.evaluate`, and `IndexedDB.*` domain calls |
|
|
27
|
+
| `IndexedDB.requestDatabaseNames` not found | Use `Runtime.evaluate` with `indexedDB.databases()` instead — it is a Promise-based browser API available in Chrome 71+ |
|
|
28
|
+
| `Target.createBrowserContext` not allowed | Requires browser-level WebSocket — not available in tab-level CDP connection |
|
|
29
|
+
| Geolocation `getCurrentPosition` hangs | Add `Browser.grantPermissions({ permissions: ["geolocation"] })` before `Emulation.setGeolocationOverride` |
|
|
30
|
+
| `CSS.enable` throws "DOM agent needs to be enabled first" | Enable `DOM` before `CSS` — order matters |
|
|
31
|
+
| Coverage shows 0 functions/rules | Target page has no JS/CSS frameworks — test on a real app page, not static HTML |
|
|
32
|
+
| **Consent / GDPR wall — page redirects to privacy dialog before content** | Detect: title in foreign language, request count < 20, no API calls seen. Fix: `const btn = [...document.querySelectorAll('button,a')].find(b => /accept\|agree\|לקבל/i.test(b.innerText\|b.textContent\|'')); if (btn) btn.click();` → wait 1500ms → re-navigate to original URL. Add this check after first `Page.navigate` settles. |
|
|
33
|
+
| Performance metrics show DNS/TCP/TLS = 0ms and all resource durations = 0ms | You are measuring a warm/cached navigation. For cold-load metrics: call `await cdp.send('Network.clearBrowserCache', {})` and `await cdp.send('Network.clearBrowserCookies', {})` before `Page.navigate`, or use `--headless` with a fresh profile (default). |
|
|
34
|
+
| FCP / First Paint is `null` after navigation | Paint entries only exist for the navigated frame. If you navigated twice (e.g. after accepting a consent wall), call `performance.getEntriesByType('paint')` immediately after the *second* navigate settles, not after waiting. **Recommended:** read `performance.getEntriesByType('paint').find(e => e.name === 'first-contentful-paint')?.startTime` — already in ms from `navigationStart`, no reference-frame conversion needed. **Alternative (CDP lifecycle):** call `Page.setLifecycleEventsEnabled({ enabled: true })` after `Page.enable`; use `'commit'` for `navStartTs` and `'firstContentfulPaint'` for FCP delta — do NOT mix with `performance.now()` (different reference frame). |
|
|
35
|
+
| JS dead-code findings are all single-letter names (`c`, `i`, `Tt`, `Ut`) | Bundle is minified — function names are mangled. Filter out names with `name.length <= 2` before emitting `[FINDING] DEAD_CODE`. To get readable names you need source maps: serve the site with `//# sourceMappingURL=` intact and use `Debugger.getScriptSource` + source map parsing. |
|
|
36
|
+
| Fetch mocking not intercepting | Call `Fetch.enable` with `patterns` BEFORE navigation — it must be active before requests start |
|
|
37
|
+
| Screenshot is blank / all black | Page not fully loaded — add a `setTimeout` wait after navigate before calling `captureScreenshot` |
|
|
38
|
+
| Heap snapshot times out | Large page — increase `--timeout` to 120000+ ms |
|
|
39
|
+
| `Network.getResponseBody` returns nothing | Body was already evicted from cache — capture the `requestId` in `Network.responseReceived` immediately |
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Script Pattern Router
|
|
2
|
+
|
|
3
|
+
Use this file to choose the smallest pattern detail file. Do not load every code-heavy pattern by default.
|
|
4
|
+
|
|
5
|
+
## Loading Rule
|
|
6
|
+
|
|
7
|
+
1. Pick the pattern that matches the missing helper or script shape.
|
|
8
|
+
2. Load only the listed `SCRIPT_PATTERNS_*.md` file and section.
|
|
9
|
+
3. Treat code as an adaptable example, not a fixed framework.
|
|
10
|
+
4. Verify current CDP params in `CDP_AGENT_REFERENCE.md`; feature-detect browser/Web APIs inside the page.
|
|
11
|
+
|
|
12
|
+
## Detail Files
|
|
13
|
+
|
|
14
|
+
| File | Patterns | Load when |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `SCRIPT_PATTERNS_OBSERVE.md` | `Network Console (most common)`, `Performance Audit`, `Core Web Vitals (inject before navigate)`, `DOM Accessibility Audit`, `Heap Memory Audit (leak detection)`, `Security Audit` | Load for console/network capture, performance, DOM, memory, and security observation patterns. |
|
|
17
|
+
| `SCRIPT_PATTERNS_BROWSER.md` | `WebSocket Surveillance`, `Search Text Across All Resources`, `File Upload`, `Save Files Screenshots PDFs and Metadata`, `Shadow DOM Querying Inside Shadow Roots`, `Source Map Resolution` | Load for WebSocket frames, page resource search, file upload, artifact saving, shadow DOM, and source-map resolution patterns. |
|
|
18
|
+
| `SCRIPT_PATTERNS_ASYNC_WORKERS.md` | `waitForNetworkIdle`, `waitForSelector with Actionability`, `Service Worker Lifecycle`, `WebSocket inside Workers` | Load for wait helpers, Service Worker lifecycle tracking, and WebSocket traffic inside worker sessions. |
|
|
19
|
+
| `SCRIPT_PATTERNS_SPECIAL.md` | `Storage Audit`, `Consent Audit`, `Full Audit (combine all)` | Load for storage, consent, and full-audit pointers that delegate to intent detail files. |
|
|
20
|
+
|
|
21
|
+
## Pattern Index
|
|
22
|
+
|
|
23
|
+
| Need | Details |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Network Console (most common) | [SCRIPT_PATTERNS_OBSERVE.md#network-console-most-common](SCRIPT_PATTERNS_OBSERVE.md#network-console-most-common) |
|
|
26
|
+
| Performance Audit | [SCRIPT_PATTERNS_OBSERVE.md#performance-audit](SCRIPT_PATTERNS_OBSERVE.md#performance-audit) |
|
|
27
|
+
| Core Web Vitals (inject before navigate) | [SCRIPT_PATTERNS_OBSERVE.md#core-web-vitals-inject-before-navigate](SCRIPT_PATTERNS_OBSERVE.md#core-web-vitals-inject-before-navigate) |
|
|
28
|
+
| DOM Accessibility Audit | [SCRIPT_PATTERNS_OBSERVE.md#dom-accessibility-audit](SCRIPT_PATTERNS_OBSERVE.md#dom-accessibility-audit) |
|
|
29
|
+
| Heap Memory Audit (leak detection) | [SCRIPT_PATTERNS_OBSERVE.md#heap-memory-audit-leak-detection](SCRIPT_PATTERNS_OBSERVE.md#heap-memory-audit-leak-detection) |
|
|
30
|
+
| Security Audit | [SCRIPT_PATTERNS_OBSERVE.md#security-audit](SCRIPT_PATTERNS_OBSERVE.md#security-audit) |
|
|
31
|
+
| WebSocket Surveillance | [SCRIPT_PATTERNS_BROWSER.md#websocket-surveillance](SCRIPT_PATTERNS_BROWSER.md#websocket-surveillance) |
|
|
32
|
+
| Search Text Across All Resources | [SCRIPT_PATTERNS_BROWSER.md#search-text-across-all-resources](SCRIPT_PATTERNS_BROWSER.md#search-text-across-all-resources) |
|
|
33
|
+
| File Upload | [SCRIPT_PATTERNS_BROWSER.md#file-upload](SCRIPT_PATTERNS_BROWSER.md#file-upload) |
|
|
34
|
+
| waitForNetworkIdle | [SCRIPT_PATTERNS_ASYNC_WORKERS.md#waitfornetworkidle](SCRIPT_PATTERNS_ASYNC_WORKERS.md#waitfornetworkidle) |
|
|
35
|
+
| waitForSelector with Actionability | [SCRIPT_PATTERNS_ASYNC_WORKERS.md#waitforselector-with-actionability](SCRIPT_PATTERNS_ASYNC_WORKERS.md#waitforselector-with-actionability) |
|
|
36
|
+
| Save Files Screenshots PDFs and Metadata | [SCRIPT_PATTERNS_BROWSER.md#save-files-screenshots-pdfs-and-metadata](SCRIPT_PATTERNS_BROWSER.md#save-files-screenshots-pdfs-and-metadata) |
|
|
37
|
+
| Shadow DOM Querying Inside Shadow Roots | [SCRIPT_PATTERNS_BROWSER.md#shadow-dom-querying-inside-shadow-roots](SCRIPT_PATTERNS_BROWSER.md#shadow-dom-querying-inside-shadow-roots) |
|
|
38
|
+
| Source Map Resolution | [SCRIPT_PATTERNS_BROWSER.md#source-map-resolution](SCRIPT_PATTERNS_BROWSER.md#source-map-resolution) |
|
|
39
|
+
| Service Worker Lifecycle | [SCRIPT_PATTERNS_ASYNC_WORKERS.md#service-worker-lifecycle](SCRIPT_PATTERNS_ASYNC_WORKERS.md#service-worker-lifecycle) |
|
|
40
|
+
| WebSocket inside Workers | [SCRIPT_PATTERNS_ASYNC_WORKERS.md#websocket-inside-workers](SCRIPT_PATTERNS_ASYNC_WORKERS.md#websocket-inside-workers) |
|
|
41
|
+
| Storage Audit | [SCRIPT_PATTERNS_SPECIAL.md#storage-audit](SCRIPT_PATTERNS_SPECIAL.md#storage-audit) |
|
|
42
|
+
| Consent Audit | [SCRIPT_PATTERNS_SPECIAL.md#consent-audit](SCRIPT_PATTERNS_SPECIAL.md#consent-audit) |
|
|
43
|
+
| Full Audit (combine all) | [SCRIPT_PATTERNS_SPECIAL.md#full-audit-combine-all](SCRIPT_PATTERNS_SPECIAL.md#full-audit-combine-all) |
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# CDP Async And Worker Pattern Details
|
|
2
|
+
|
|
3
|
+
## waitForNetworkIdle
|
|
4
|
+
|
|
5
|
+
Event-driven wait until all in-flight network requests finish. More reliable than `setTimeout` because it tracks every `requestWillBeSent` / `loadingFinished` pair, including late XHR/fetch calls fired by page JS after DOM load.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// Requires: Network.enable (already active)
|
|
9
|
+
// Attach listeners BEFORE navigating — events fire immediately on navigation start
|
|
10
|
+
|
|
11
|
+
async function waitForNetworkIdle(cdp, { idleMs = 500, timeoutMs = 30000 } = {}) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let pending = 0;
|
|
14
|
+
let idleTimer = null;
|
|
15
|
+
|
|
16
|
+
const deadline = setTimeout(() => {
|
|
17
|
+
clearTimeout(idleTimer);
|
|
18
|
+
reject(new Error(`waitForNetworkIdle timed out after ${timeoutMs}ms`));
|
|
19
|
+
}, timeoutMs);
|
|
20
|
+
|
|
21
|
+
const scheduleIdle = () => {
|
|
22
|
+
clearTimeout(idleTimer);
|
|
23
|
+
if (pending === 0) {
|
|
24
|
+
idleTimer = setTimeout(() => { clearTimeout(deadline); resolve(); }, idleMs);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
cdp.on('Network.requestWillBeSent', () => { pending++; clearTimeout(idleTimer); });
|
|
29
|
+
cdp.on('Network.loadingFinished', () => { pending = Math.max(0, pending - 1); scheduleIdle(); });
|
|
30
|
+
cdp.on('Network.loadingFailed', () => { pending = Math.max(0, pending - 1); scheduleIdle(); });
|
|
31
|
+
cdp.on('Network.requestServedFromCache', () => { pending = Math.max(0, pending - 1); scheduleIdle(); });
|
|
32
|
+
|
|
33
|
+
scheduleIdle(); // resolve immediately if nothing is already in-flight
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function run(cdp) {
|
|
38
|
+
await cdp.send('Network.enable', {});
|
|
39
|
+
await cdp.send('Page.enable', {});
|
|
40
|
+
|
|
41
|
+
// Attach idle listener BEFORE navigating
|
|
42
|
+
const idlePromise = waitForNetworkIdle(cdp, { idleMs: 500, timeoutMs: 15000 });
|
|
43
|
+
|
|
44
|
+
await cdp.send('Page.navigate', { url: 'https://example.com' });
|
|
45
|
+
await idlePromise;
|
|
46
|
+
|
|
47
|
+
console.log('[METRIC] Network idle — all requests finished');
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Alternative: `Page.lifecycleEvent` (simpler, less precise)**
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
await cdp.send('Page.setLifecycleEventsEnabled', { enabled: true });
|
|
55
|
+
await new Promise(resolve =>
|
|
56
|
+
cdp.on('Page.lifecycleEvent', ({ name }) => {
|
|
57
|
+
if (name === 'networkIdle') resolve();
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Parameters:**
|
|
63
|
+
- `idleMs` (default 500ms): quiet window — how long zero pending requests must hold before resolving
|
|
64
|
+
- `timeoutMs` (default 30000ms): hard ceiling — rejects if network never goes idle
|
|
65
|
+
|
|
66
|
+
**`requestServedFromCache` note:** cache hits never fire `loadingFinished`, so they must be counted as completions separately.
|
|
67
|
+
|
|
68
|
+
## waitForSelector with Actionability
|
|
69
|
+
|
|
70
|
+
Wait for an element to be present **and** actionable — visible, enabled, and reachable by pointer. Prevents premature clicks on elements that exist in the DOM but are animating, hidden behind overlays, or inside disabled fieldsets.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
// Requires: Runtime.enable, DOM.enable
|
|
74
|
+
// Checks: in DOM + non-zero size + not display:none/visibility:hidden/opacity:0 + not disabled + pointer-events != none
|
|
75
|
+
|
|
76
|
+
async function waitForSelector(cdp, selector, {
|
|
77
|
+
timeoutMs = 10000,
|
|
78
|
+
checkVisible = true, // non-zero bounding box + not hidden via CSS
|
|
79
|
+
checkEnabled = true, // not disabled (self or ancestor)
|
|
80
|
+
checkPointer = true, // CSS pointer-events !== 'none'
|
|
81
|
+
pollMs = 150,
|
|
82
|
+
} = {}) {
|
|
83
|
+
const deadline = Date.now() + timeoutMs;
|
|
84
|
+
|
|
85
|
+
while (Date.now() < deadline) {
|
|
86
|
+
const { result } = await cdp.send('Runtime.evaluate', {
|
|
87
|
+
expression: `(function() {
|
|
88
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
89
|
+
if (!el) return null;
|
|
90
|
+
const s = window.getComputedStyle(el);
|
|
91
|
+
const r = el.getBoundingClientRect();
|
|
92
|
+
return {
|
|
93
|
+
visible: r.width > 0 && r.height > 0
|
|
94
|
+
&& s.display !== 'none'
|
|
95
|
+
&& s.visibility !== 'hidden'
|
|
96
|
+
&& s.opacity !== '0',
|
|
97
|
+
enabled: !el.disabled && !el.closest('[disabled]'),
|
|
98
|
+
pointer: s.pointerEvents !== 'none',
|
|
99
|
+
};
|
|
100
|
+
})()`,
|
|
101
|
+
returnByValue: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const state = result.value;
|
|
105
|
+
if (!state) { await new Promise(r => setTimeout(r, pollMs)); continue; }
|
|
106
|
+
|
|
107
|
+
const ready = (!checkVisible || state.visible)
|
|
108
|
+
&& (!checkEnabled || state.enabled)
|
|
109
|
+
&& (!checkPointer || state.pointer);
|
|
110
|
+
|
|
111
|
+
if (ready) {
|
|
112
|
+
const { root } = await cdp.send('DOM.getDocument', { depth: 0 });
|
|
113
|
+
const { nodeId } = await cdp.send('DOM.querySelector', { nodeId: root.nodeId, selector });
|
|
114
|
+
console.log(`[AUTOMATE] "${selector}" ready — visible:${state.visible} enabled:${state.enabled} pointer:${state.pointer}`);
|
|
115
|
+
return nodeId; // use with DOM.getBoxModel + Input.dispatchMouseEvent to click
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await new Promise(r => setTimeout(r, pollMs));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new Error(`waitForSelector("${selector}") timed out after ${timeoutMs}ms`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Usage: click a button only once it is visible and enabled
|
|
125
|
+
export async function run(cdp) {
|
|
126
|
+
await cdp.send('DOM.enable', {});
|
|
127
|
+
await cdp.send('Runtime.enable', {});
|
|
128
|
+
await cdp.send('Page.enable', {});
|
|
129
|
+
await cdp.send('Page.navigate', { url: 'https://example.com' });
|
|
130
|
+
|
|
131
|
+
const nodeId = await waitForSelector(cdp, '#submit-btn');
|
|
132
|
+
|
|
133
|
+
// Real mouse click via CDP Input (required for some frameworks)
|
|
134
|
+
await cdp.send('DOM.scrollIntoViewIfNeeded', { nodeId });
|
|
135
|
+
const { model } = await cdp.send('DOM.getBoxModel', { nodeId });
|
|
136
|
+
const [x1,,x3,,,,,y1,,,,y3] = model.content; // 8-point polygon
|
|
137
|
+
const cx = (x1 + x3) / 2, cy = (y1 + y3) / 2;
|
|
138
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1 });
|
|
139
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1 });
|
|
140
|
+
console.log('[AUTOMATE] clicked #submit-btn');
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Actionability checks explained:**
|
|
145
|
+
- **visible**: `getBoundingClientRect().width/height > 0` AND not `display:none` / `visibility:hidden` / `opacity:0`
|
|
146
|
+
- **enabled**: `!el.disabled` AND not inside a `[disabled]` fieldset/ancestor
|
|
147
|
+
- **pointer**: `pointer-events !== 'none'` — catches elements blocked by CSS overlays
|
|
148
|
+
|
|
149
|
+
**Skip flags:**
|
|
150
|
+
- `checkVisible: false` — for off-screen or hidden inputs you set by value, not click
|
|
151
|
+
- `checkEnabled: false` — for read-only fields
|
|
152
|
+
- `checkPointer: false` — for elements that use JS click handlers bypassing CSS pointer-events
|
|
153
|
+
|
|
154
|
+
## Service Worker Lifecycle
|
|
155
|
+
|
|
156
|
+
Tracks every Service Worker state transition via the `ServiceWorker` CDP domain. Captures registration, full lifecycle (new → installing → installed → activating → activated → redundant), and the navigator API snapshot.
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
export async function run(cdp) {
|
|
160
|
+
await cdp.send('Network.enable', {});
|
|
161
|
+
await cdp.send('Page.enable', {});
|
|
162
|
+
await cdp.send('Page.setLifecycleEventsEnabled', { enabled: true });
|
|
163
|
+
await cdp.send('ServiceWorker.enable', {}); // must be before navigation
|
|
164
|
+
|
|
165
|
+
const swVersions = new Map(); // scriptURL → latest status/runningStatus key
|
|
166
|
+
const swRegistrations = new Set(); // scopeURLs seen
|
|
167
|
+
|
|
168
|
+
cdp.on('ServiceWorker.workerRegistrationUpdated', ({ registrations }) => {
|
|
169
|
+
for (const r of registrations) {
|
|
170
|
+
if (r.scopeURL.startsWith('chrome-extension:')) continue;
|
|
171
|
+
const label = r.isDeleted ? 'DELETED' : 'REGISTERED';
|
|
172
|
+
console.log(`[SW] ${label}: scope=${r.scopeURL} id=${r.registrationId}`);
|
|
173
|
+
if (!r.isDeleted) {
|
|
174
|
+
swRegistrations.add(r.scopeURL);
|
|
175
|
+
console.log(`[FINDING] SW_REGISTERED: ${r.scopeURL}`);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(`[FINDING] SW_REMOVED: ${r.scopeURL}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
cdp.on('ServiceWorker.workerVersionUpdated', ({ versions }) => {
|
|
183
|
+
for (const v of versions) {
|
|
184
|
+
if (!v.scriptURL || v.scriptURL.startsWith('chrome-extension:')) continue;
|
|
185
|
+
const key = `${v.status}/${v.runningStatus}`;
|
|
186
|
+
const prev = swVersions.get(v.scriptURL);
|
|
187
|
+
if (prev === key) continue; // deduplicate repeated events
|
|
188
|
+
swVersions.set(v.scriptURL, key);
|
|
189
|
+
const fn = v.scriptURL.split('/').slice(-1)[0].split('?')[0];
|
|
190
|
+
console.log(`[SW] VERSION: ${fn} → ${v.status}/${v.runningStatus} (script=${v.scriptURL})`);
|
|
191
|
+
if (v.status === 'activated') console.log(`[FINDING] SW_ACTIVATED: ${v.scriptURL}`);
|
|
192
|
+
if (v.status === 'redundant') console.log(`[FINDING] SW_REDUNDANT: ${v.scriptURL}`);
|
|
193
|
+
const swHost = (() => { try { return new URL(v.scriptURL).hostname; } catch { return ''; } })();
|
|
194
|
+
const pageHost = (() => { try { return new URL(TARGET_URL).hostname; } catch { return ''; } })();
|
|
195
|
+
if (swHost && pageHost && swHost !== pageHost) console.log(`[FINDING] SW_THIRD_PARTY_SCRIPT: ${v.scriptURL}`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await cdp.send('Page.navigate', { url: TARGET_URL });
|
|
200
|
+
await new Promise(r => setTimeout(r, 15000)); // allow SW to install + activate
|
|
201
|
+
|
|
202
|
+
// Point-in-time snapshot via navigator API
|
|
203
|
+
const { result } = await cdp.send('Runtime.evaluate', {
|
|
204
|
+
expression: `(async () => {
|
|
205
|
+
if (!navigator.serviceWorker) return '[]';
|
|
206
|
+
const regs = await navigator.serviceWorker.getRegistrations();
|
|
207
|
+
return JSON.stringify(regs.map(r => ({
|
|
208
|
+
scope: r.scope,
|
|
209
|
+
updateViaCache: r.updateViaCache,
|
|
210
|
+
state: (r.active || r.installing || r.waiting)?.state ?? 'unknown',
|
|
211
|
+
scriptURL: (r.active || r.installing || r.waiting)?.scriptURL ?? null,
|
|
212
|
+
})));
|
|
213
|
+
})()`,
|
|
214
|
+
awaitPromise: true, returnByValue: false,
|
|
215
|
+
});
|
|
216
|
+
const sws = JSON.parse(result.value ?? '[]');
|
|
217
|
+
|
|
218
|
+
console.log(`[METRIC] SW registrations (live CDP): ${swRegistrations.size}`);
|
|
219
|
+
console.log(`[METRIC] SW registrations (navigator API): ${sws.length}`);
|
|
220
|
+
for (const s of sws) {
|
|
221
|
+
console.log(`[SW] SNAPSHOT: scope=${s.scope} state=${s.state} script=${s.scriptURL}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await cdp.send('ServiceWorker.disable', {});
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Key facts:**
|
|
229
|
+
- `ServiceWorker.enable` must be called before navigation — events are not replayed retroactively
|
|
230
|
+
- `workerVersionUpdated` fires repeatedly for the same version as state advances — deduplicate on `scriptURL + status/runningStatus`
|
|
231
|
+
- `workerRegistrationUpdated` fires for ALL known registrations (including from prior sessions, extensions) on enable — filter `chrome-extension:` scopes
|
|
232
|
+
- `status` lifecycle order: `new` → `installing` → `installed` → `activating` → `activated` → `redundant`
|
|
233
|
+
- `runningStatus` lifecycle: `stopped` → `starting` → `running` → `stopping`
|
|
234
|
+
- Use `ServiceWorker.skipWaiting({scopeURL})` to force a waiting SW to activate immediately (useful in tests)
|
|
235
|
+
|
|
236
|
+
## WebSocket inside Workers
|
|
237
|
+
|
|
238
|
+
Web Workers and Shared Workers can open their own WebSocket connections. These do **not** emit `Network.webSocketCreated` on the main session — you must attach to each worker via `Target.setAutoAttach` and enable `Network` on its session.
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
export async function run(cdp) {
|
|
242
|
+
await cdp.send('Network.enable', {});
|
|
243
|
+
await cdp.send('Page.enable', {});
|
|
244
|
+
await cdp.send('Page.setLifecycleEventsEnabled', { enabled: true });
|
|
245
|
+
await cdp.send('ServiceWorker.enable', {});
|
|
246
|
+
// flatten:true = worker sessions share the same CDP WebSocket; sessionId routes commands
|
|
247
|
+
await cdp.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
248
|
+
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
249
|
+
|
|
250
|
+
const wsMap = new Map(); // `${sessionLabel}::${requestId}` → { url, frames }
|
|
251
|
+
|
|
252
|
+
// Helper: attach Network WS listeners to any session (main page OR worker)
|
|
253
|
+
function listenWS(session, label) {
|
|
254
|
+
function on(event, handler) {
|
|
255
|
+
cdp.on(event, (params, meta) => {
|
|
256
|
+
// main page: no meta.sessionId; worker: meta.sessionId matches
|
|
257
|
+
const isTarget = label === 'MAIN'
|
|
258
|
+
? !meta?.sessionId
|
|
259
|
+
: meta?.sessionId === session.sessionId;
|
|
260
|
+
if (isTarget) handler(params);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
on('Network.webSocketCreated', ({ requestId, url }) => {
|
|
265
|
+
wsMap.set(`${label}::${requestId}`, { url, sent: [], recv: [] });
|
|
266
|
+
console.log(`[WORKER] WS OPENED [${label}]: ${url}`);
|
|
267
|
+
const wsHost = (() => { try { return new URL(url).hostname; } catch { return '?'; } })();
|
|
268
|
+
console.log(`[FINDING] WS_IN_WORKER: type=${label} host=${wsHost} url=${url}`);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
on('Network.webSocketFrameSent', ({ requestId, response }) => {
|
|
272
|
+
const ws = wsMap.get(`${label}::${requestId}`);
|
|
273
|
+
if (!ws) return;
|
|
274
|
+
const p = response?.payloadData ?? '';
|
|
275
|
+
ws.sent.push({ bytes: p.length, opcode: response?.opcode });
|
|
276
|
+
console.log(`[WORKER] WS SENT [${label}] ${p.length}B op=${response?.opcode}`);
|
|
277
|
+
if (/token|password|secret|key|auth|jwt/i.test(p))
|
|
278
|
+
console.log(`[FINDING] SENSITIVE_IN_WS_SENT [${label}]: value redacted`);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
on('Network.webSocketFrameReceived', ({ requestId, response }) => {
|
|
282
|
+
const ws = wsMap.get(`${label}::${requestId}`);
|
|
283
|
+
if (!ws) return;
|
|
284
|
+
const p = response?.payloadData ?? '';
|
|
285
|
+
ws.recv.push({ bytes: p.length, opcode: response?.opcode });
|
|
286
|
+
console.log(`[WORKER] WS RECV [${label}] ${p.length}B op=${response?.opcode}`);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
on('Network.webSocketClosed', ({ requestId }) => {
|
|
290
|
+
const ws = wsMap.get(`${label}::${requestId}`);
|
|
291
|
+
if (!ws) return;
|
|
292
|
+
console.log(`[WORKER] WS CLOSED [${label}]: ${ws.url} S:${ws.sent.length} R:${ws.recv.length}`);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Listen on the main page session
|
|
297
|
+
listenWS({ sessionId: null }, 'MAIN');
|
|
298
|
+
|
|
299
|
+
// When a worker attaches, enable Network on it and listen there too
|
|
300
|
+
let workerCount = 0;
|
|
301
|
+
cdp.on('Target.attachedToTarget', async ({ targetInfo, sessionId }) => {
|
|
302
|
+
const t = targetInfo.type;
|
|
303
|
+
if (!['worker', 'shared_worker', 'service_worker'].includes(t)) return;
|
|
304
|
+
workerCount++;
|
|
305
|
+
const label = `${t.toUpperCase()}(${workerCount})`;
|
|
306
|
+
console.log(`[WORKER] Attached: type=${t} label=${label} url=${targetInfo.url || '(blob)'}`);
|
|
307
|
+
console.log(`[FINDING] WORKER_${t.toUpperCase()}: ${targetInfo.url || '(blob)'}`);
|
|
308
|
+
try {
|
|
309
|
+
await cdp.send('Network.enable', {}, sessionId); // route to worker session
|
|
310
|
+
listenWS({ sessionId }, label);
|
|
311
|
+
} catch (e) {
|
|
312
|
+
console.log(`[WORKER] Network.enable failed for ${label}: ${e.message}`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
cdp.on('Page.javascriptDialogOpening', () => cdp.send('Page.handleJavaScriptDialog', { accept: true }));
|
|
317
|
+
|
|
318
|
+
await cdp.send('Page.navigate', { url: TARGET_URL });
|
|
319
|
+
await new Promise(r => setTimeout(r, 20000));
|
|
320
|
+
|
|
321
|
+
// Summary
|
|
322
|
+
const allWS = [...wsMap.values()];
|
|
323
|
+
console.log(`[METRIC] Total WS connections (main + workers): ${allWS.length}`);
|
|
324
|
+
for (const ws of allWS) {
|
|
325
|
+
console.log(`[METRIC] WS: ${ws.url} sent:${ws.sent.length} recv:${ws.recv.length}`);
|
|
326
|
+
}
|
|
327
|
+
const { targetInfos } = await cdp.send('Target.getTargets', {});
|
|
328
|
+
const wTargets = targetInfos.filter(t => ['worker','shared_worker','service_worker'].includes(t.type));
|
|
329
|
+
console.log(`[METRIC] Worker targets: ${wTargets.length}`);
|
|
330
|
+
for (const t of wTargets) console.log(`[METRIC] type=${t.type} url=${t.url || '(blob)'}`);
|
|
331
|
+
|
|
332
|
+
// Cleanup
|
|
333
|
+
await cdp.send('ServiceWorker.disable', {});
|
|
334
|
+
await cdp.send('Target.setAutoAttach', { autoAttach: false, waitForDebuggerOnStart: false, flatten: true });
|
|
335
|
+
await cdp.send('Target.setDiscoverTargets', { discover: false });
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**Key facts:**
|
|
340
|
+
- `flatten: true` on `setAutoAttach` is required — it makes all sessions share one WS connection and routes via `sessionId`
|
|
341
|
+
- Pass `sessionId` as the **third argument** to `cdp.send(method, params, sessionId)` to send commands to a worker session
|
|
342
|
+
- Worker `Network.*` events arrive on the main `cdp.on()` listener but carry a `meta.sessionId` in the second argument — check it to route correctly
|
|
343
|
+
- Workers spawned as `blob:` URLs cannot be read by source, but their network traffic (HTTP + WS) is fully capturable
|
|
344
|
+
- `Target.setAutoAttach` with `waitForDebuggerOnStart: false` lets workers run normally — set to `true` only if you need to pause a worker at startup for debugging
|
|
345
|
+
- `shared_worker` type means a single worker shared across tabs — it persists even after the page navigates
|