github-router 0.3.52 → 0.3.66

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.
@@ -3769,7 +3769,13 @@ const httpServer = createServer((req, res) => {
3769
3769
  ok: true,
3770
3770
  pid: process$1.pid,
3771
3771
  extension_connected: extensionConnected(),
3772
- extension_loaded_version: extensionLoadedVersion
3772
+ extension_loaded_version: extensionLoadedVersion,
3773
+ humanlike_tabs: Array.from(humanlikeTabs.entries()).map(([tabId, v]) => ({
3774
+ tabId,
3775
+ vendor: v.vendor,
3776
+ signal: v.signal,
3777
+ since: v.since
3778
+ }))
3773
3779
  }));
3774
3780
  return;
3775
3781
  }
@@ -3858,10 +3864,20 @@ fromBrowserListeners.push((msg) => {
3858
3864
  extensionLoadedVersion = msg.version;
3859
3865
  return;
3860
3866
  }
3867
+ if (msg && typeof msg === "object" && msg.type === "__botDetected__" && typeof msg.tabId === "number") {
3868
+ const m = msg;
3869
+ humanlikeTabs.set(m.tabId, {
3870
+ vendor: typeof m.vendor === "string" ? m.vendor : "unknown",
3871
+ signal: typeof m.signal === "string" ? m.signal : "",
3872
+ since: Date.now()
3873
+ });
3874
+ return;
3875
+ }
3861
3876
  const r = msg;
3862
3877
  if (typeof r.id !== "string") return;
3863
3878
  pendingResolve(r.id, r);
3864
3879
  });
3880
+ const humanlikeTabs = /* @__PURE__ */ new Map();
3865
3881
  httpServer.listen(0, "127.0.0.1", () => {
3866
3882
  const addr = httpServer.address();
3867
3883
  if (!addr || typeof addr === "string") {
@@ -20,6 +20,15 @@
20
20
 
21
21
  const NATIVE_HOST_NAME = "com.githubrouter.browser"
22
22
 
23
+ // Snapshot cache + invalidation lives in a sibling module so the
24
+ // matcher-cascade work in Phase 2 can consume it without dragging in
25
+ // the entire 1700-line background.js dispatcher.
26
+ import {
27
+ captureSnapshot,
28
+ invalidateSnapshot,
29
+ } from "./snapshot.js"
30
+ import { extractSnapshotCDP } from "./snapshot-cdp.js"
31
+
23
32
  // ---------------------------------------------------------------------
24
33
  // Navigation policy — URL patterns this extension blocks at
25
34
  // webNavigation.onBeforeNavigate. This list is INTENTIONALLY NARROWER
@@ -185,6 +194,40 @@ async function toolReadPage(args) {
185
194
  const tabId = typeof args.tabId === "number" ? args.tabId : undefined
186
195
  if (!tabId) throw new Error("browser_read_page: tabId is required")
187
196
  const mode = args.mode === "full" ? "full" : "summary"
197
+ const refresh = args.refresh === true
198
+ // Phase 1c-CDP: try the CDP `Accessibility.getFullAXTree`-based
199
+ // extractor first. Better cross-origin iframe coverage, real
200
+ // platform-computed accessible names, AX-tree-flagged hidden /
201
+ // disabled state. Falls back to the legacy DOM walker when CDP
202
+ // attach fails (enterprise DeveloperToolsAvailability=2, DevTools
203
+ // already open on the tab, sandbox restriction). The fallback path
204
+ // produces a strict-subset shape so consumers don't have to branch.
205
+ const extractor = async (tId, ext) => {
206
+ try {
207
+ return await extractSnapshotCDP(tId, ext, {
208
+ attachDebugger: attachDebuggerOnce,
209
+ sendCommand: (id, method, params) => chrome.debugger.sendCommand({ tabId: id }, method, params),
210
+ })
211
+ } catch (err) {
212
+ const msg = err && err.message ? err.message : String(err)
213
+ console.warn(`[browser-mcp/snapshot] CDP extractor failed, falling back to legacy: ${msg}`)
214
+ return await extractSnapshotLegacy(tId, ext)
215
+ }
216
+ }
217
+ return captureSnapshot(tabId, { mode, refresh }, extractor)
218
+ }
219
+
220
+ /**
221
+ * Legacy `document.querySelectorAll`-based extractor. Stays as the
222
+ * default extractor until Phase 1b-CDP lands; will become the fallback
223
+ * path when CDP attach fails (enterprise policy, DevTools open on the
224
+ * tab, etc.). The implementation runs in the page world via
225
+ * chrome.scripting.executeScript and returns a PageSnapshot-shaped
226
+ * object that snapshot.captureSnapshot caches and returns to the
227
+ * caller.
228
+ */
229
+ async function extractSnapshotLegacy(tabId, opts) {
230
+ const mode = opts?.mode === "full" ? "full" : "summary"
188
231
  const [result] = await chrome.scripting.executeScript({
189
232
  target: { tabId },
190
233
  func: (mode) => {
@@ -303,6 +346,97 @@ async function toolReadPage(args) {
303
346
  // elements (a div with role="button" but no aria-label is noise).
304
347
  // Full mode: keep everything, model asked for it.
305
348
  const ELEMENT_CAP = 200
349
+ const LANDMARK_ROLES = new Set([
350
+ "dialog", "alertdialog", "region", "navigation", "main",
351
+ "form", "search", "complementary", "banner", "contentinfo",
352
+ ])
353
+ const LANDMARK_TAGS = new Set([
354
+ "dialog", "form", "nav", "main", "header", "footer", "aside", "section",
355
+ ])
356
+ // Pre-mint refs for landmark ancestors so child elements can
357
+ // cite parent refs without a second walk.
358
+ function landmarkRefsFor(el) {
359
+ const refs = []
360
+ let cur = el.parentElement
361
+ let depth = 0
362
+ while (cur && depth < 12 && refs.length < 4) {
363
+ const role = cur.getAttribute && cur.getAttribute("role")
364
+ const ctag = cur.tagName && cur.tagName.toLowerCase()
365
+ const isLandmark = (role && LANDMARK_ROLES.has(role)) || LANDMARK_TAGS.has(ctag)
366
+ if (isLandmark) {
367
+ let r = cur.getAttribute("data-gh-router-ref")
368
+ if (!r || !/^e\d+$/.test(r)) {
369
+ r = nextFreshRef()
370
+ cur.setAttribute("data-gh-router-ref", r)
371
+ }
372
+ refs.push(r)
373
+ }
374
+ cur = cur.parentElement
375
+ depth++
376
+ }
377
+ return refs
378
+ }
379
+ function stateFlagsFor(el, tag) {
380
+ const flags = {}
381
+ // disabled: prefer the property (more reliable than the attr
382
+ // for inputs / buttons; aria-disabled covers role=button divs).
383
+ if (el.disabled === true || el.getAttribute("aria-disabled") === "true") flags.disabled = true
384
+ if (el.checked === true) flags.checked = true
385
+ else if (el.indeterminate === true) flags.checked = "mixed"
386
+ else if (el.getAttribute("aria-checked") === "true") flags.checked = true
387
+ else if (el.getAttribute("aria-checked") === "mixed") flags.checked = "mixed"
388
+ const aria = (name) => el.getAttribute(name)
389
+ if (aria("aria-expanded") === "true") flags.expanded = true
390
+ else if (aria("aria-expanded") === "false") flags.expanded = false
391
+ if (el.selected === true || aria("aria-selected") === "true") flags.selected = true
392
+ if (aria("aria-pressed") === "true") flags.pressed = true
393
+ else if (aria("aria-pressed") === "false") flags.pressed = false
394
+ if (el.required === true || aria("aria-required") === "true") flags.required = true
395
+ if (el.readOnly === true || aria("aria-readonly") === "true") flags.readonly = true
396
+ if (aria("aria-invalid") === "true") flags.invalid = true
397
+ if (document.activeElement === el) flags.focused = true
398
+ // hidden: aria-hidden takes precedence; offsetParent === null
399
+ // covers display:none parents (NOT a reliable visibility check
400
+ // for fixed-position elements but a reasonable cheap signal).
401
+ if (aria("aria-hidden") === "true") flags.hidden = true
402
+ else if (tag !== "body" && el.offsetParent === null && getComputedStyle(el).position !== "fixed") {
403
+ flags.hidden = true
404
+ }
405
+ return flags
406
+ }
407
+ function inputExtrasFor(el, tag) {
408
+ const out = {}
409
+ if (tag === "input" || tag === "textarea" || tag === "select") {
410
+ const t = (el.type || "").toLowerCase()
411
+ if (t) out.inputType = t
412
+ }
413
+ const ph = el.placeholder || el.getAttribute("placeholder")
414
+ if (ph) out.placeholder = String(ph).slice(0, 200)
415
+ const ac = el.getAttribute("autocomplete")
416
+ if (ac) out.autocomplete = ac
417
+ // For inputs / textareas / select, value is the current user
418
+ // input. Bounded so a huge textarea doesn't bloat the snapshot.
419
+ if (typeof el.value === "string" && el.value.length > 0) {
420
+ out.value = el.value.slice(0, 200)
421
+ }
422
+ return out
423
+ }
424
+ function attrExtrasFor(el) {
425
+ // Surface raw attrs the matcher's L5 testid layer + L7 semantic
426
+ // heuristic want to see. Limited to a handful — we don't want
427
+ // to dump every attribute on every element.
428
+ const out = {}
429
+ const id = el.id
430
+ if (id) out.id = id
431
+ const testid = el.getAttribute("data-testid") || el.getAttribute("data-test-id")
432
+ || el.getAttribute("data-test") || el.getAttribute("data-qa")
433
+ if (testid) out.testid = testid
434
+ const nameAttr = el.getAttribute("name")
435
+ if (nameAttr) out.name_attr = nameAttr
436
+ const aria = el.getAttribute("aria-label")
437
+ if (aria) out.aria_label = aria
438
+ return out
439
+ }
306
440
  const elements = []
307
441
  for (const el of interactive) {
308
442
  if (elements.length >= ELEMENT_CAP) break
@@ -319,6 +453,7 @@ async function toolReadPage(args) {
319
453
  const entry = {
320
454
  ref,
321
455
  role: el.getAttribute("role") || tag,
456
+ tag,
322
457
  bbox: [
323
458
  Math.round(rect.x),
324
459
  Math.round(rect.y),
@@ -327,6 +462,25 @@ async function toolReadPage(args) {
327
462
  ],
328
463
  }
329
464
  if (name) entry.name = name
465
+ // Inline state flags onto the entry. Each is omitted when
466
+ // false / default per the snapshot-types contract.
467
+ const flags = stateFlagsFor(el, tag)
468
+ for (const k of Object.keys(flags)) entry[k] = flags[k]
469
+ // Input-shaped extras (placeholder / inputType / value /
470
+ // autocomplete) — only present for input-shaped elements.
471
+ const inExtras = inputExtrasFor(el, tag)
472
+ if (inExtras.inputType) entry.inputType = inExtras.inputType
473
+ if (inExtras.placeholder) entry.placeholder = inExtras.placeholder
474
+ if (inExtras.autocomplete) entry.autocomplete = inExtras.autocomplete
475
+ if (inExtras.value) entry.value = inExtras.value
476
+ // Raw attribute extras for L5 testid + L7 semantic layers.
477
+ // Stored on a single `attrs` object to keep the top-level
478
+ // shape stable.
479
+ const attrExtras = attrExtrasFor(el)
480
+ if (Object.keys(attrExtras).length > 0) entry.attrs = attrExtras
481
+ // Landmark ancestry — up to 4 deep, dialog / form / nav / etc.
482
+ const landmarks = landmarkRefsFor(el)
483
+ if (landmarks.length > 0) entry.landmarks = landmarks
330
484
  elements.push(entry)
331
485
  }
332
486
  // Text extraction.
@@ -397,7 +551,14 @@ async function toolReadPage(args) {
397
551
  ],
398
552
  })
399
553
  }
400
- const out = { mode, text, elements, viewport }
554
+ const out = {
555
+ mode,
556
+ url: window.location.href,
557
+ title: document.title,
558
+ text,
559
+ elements,
560
+ viewport,
561
+ }
401
562
  if (visualSurfaces.length > 0) out.visualSurfaces = visualSurfaces
402
563
  return out
403
564
  },
@@ -1535,9 +1696,29 @@ async function attachDebuggerOnce(tabId, opts) {
1535
1696
  networkBuffers.set(tabId, [])
1536
1697
  await chrome.debugger.sendCommand({ tabId }, "Network.enable")
1537
1698
  }
1699
+ // CDP a11y-tree extraction needs DOM + Page + Accessibility
1700
+ // domains enabled. We track them in a single Set so a second
1701
+ // captureSnapshot call on the same tab is a no-op.
1702
+ if (opts?.accessibility && !axDomainsEnabledTabs.has(tabId)) {
1703
+ axDomainsEnabledTabs.add(tabId)
1704
+ try {
1705
+ await chrome.debugger.sendCommand({ tabId }, "DOM.enable")
1706
+ await chrome.debugger.sendCommand({ tabId }, "Page.enable")
1707
+ await chrome.debugger.sendCommand({ tabId }, "Accessibility.enable")
1708
+ } catch (err) {
1709
+ // Roll back the tracking flag so the next call retries.
1710
+ axDomainsEnabledTabs.delete(tabId)
1711
+ throw err
1712
+ }
1713
+ }
1538
1714
  })
1539
1715
  }
1540
1716
 
1717
+ // Track which tabs have the CDP a11y domains enabled (DOM + Page +
1718
+ // Accessibility). Cleared on debugger.onDetach / tabs.onRemoved
1719
+ // alongside the other per-tab state.
1720
+ const axDomainsEnabledTabs = new Set()
1721
+
1541
1722
  chrome.debugger.onEvent.addListener((source, method, params) => {
1542
1723
  const tabId = source.tabId
1543
1724
  if (typeof tabId !== "number") return
@@ -1569,6 +1750,12 @@ chrome.debugger.onDetach.addListener((source) => {
1569
1750
  consoleBuffers.delete(source.tabId)
1570
1751
  networkBuffers.delete(source.tabId)
1571
1752
  tabInputLockTails.delete(source.tabId)
1753
+ axDomainsEnabledTabs.delete(source.tabId)
1754
+ // Snapshot cache: CDP-written refs survive a detach (they're DOM
1755
+ // attributes, not CDP state), but bbox/AXNode IDs become unreliable
1756
+ // because re-attach needs a fresh DOM.enable handshake. Safer to
1757
+ // invalidate and re-capture on next read.
1758
+ invalidateSnapshot(source.tabId, "debugger-detach")
1572
1759
  }
1573
1760
  })
1574
1761
 
@@ -1583,6 +1770,22 @@ chrome.tabs.onRemoved.addListener((tabId) => {
1583
1770
  consoleBuffers.delete(tabId)
1584
1771
  networkBuffers.delete(tabId)
1585
1772
  tabInputLockTails.delete(tabId)
1773
+ axDomainsEnabledTabs.delete(tabId)
1774
+ invalidateSnapshot(tabId, "tab-closed")
1775
+ })
1776
+
1777
+ // Snapshot cache invalidation on top-frame navigation. The legacy ref
1778
+ // scheme (data-gh-router-ref DOM attribute) does NOT survive a fresh
1779
+ // document load, so a stale snapshot would return refs that resolve
1780
+ // to nothing. Invalidate so the next read captures the new document.
1781
+ // We intentionally do NOT invalidate on child-frame navigations — a
1782
+ // click inside an iframe shouldn't bust the whole-tab snapshot. Phase
1783
+ // 1b-CDP will revisit this when cross-origin iframe ref attribution
1784
+ // changes the trade-off.
1785
+ chrome.webNavigation.onCommitted.addListener((details) => {
1786
+ if (details.frameId === 0 && typeof details.tabId === "number") {
1787
+ invalidateSnapshot(details.tabId, "navigation")
1788
+ }
1586
1789
  })
1587
1790
 
1588
1791
  async function toolConsoleLogs(args) {
@@ -1608,6 +1811,128 @@ async function toolNetworkLog(args) {
1608
1811
  return { entries: drained }
1609
1812
  }
1610
1813
 
1814
+ // ---------------------------------------------------------------------
1815
+ // Bot-challenge detection (Phase 4 auto-detect)
1816
+ // ---------------------------------------------------------------------
1817
+ // Listens to chrome.webRequest.onHeadersReceived for response-header
1818
+ // fingerprints of major bot-protection vendors. On match: post a
1819
+ // `__botDetected__` control frame to the bridge. The bridge tracks
1820
+ // which tabs are flagged and the proxy dispatcher consults that state
1821
+ // via /health to inject humanlike pacing for paced tabs.
1822
+ //
1823
+ // Signature confidence tiers:
1824
+ // HIGH (per-vendor, single-hit enables): cf-ray + 403/503, x-dd-b,
1825
+ // x-px-block, x-px-uuid, x-incapsula header on 403.
1826
+ // MEDIUM (cookie / generic — deferred to v2): _abck=*~-1~ cookie,
1827
+ // burst of 403/429 across 5 s window.
1828
+ //
1829
+ // False-positive guard: only fires when we actually own the
1830
+ // connection to the bridge (`nativePort` set). No phantom signals
1831
+ // during SW startup before the port opens.
1832
+
1833
+ const BOT_DETECTION_VENDORS = {
1834
+ cloudflare: (resp) => {
1835
+ if (resp.statusCode !== 403 && resp.statusCode !== 503) return null
1836
+ const cfRay = headerValue(resp.responseHeaders, "cf-ray")
1837
+ return cfRay ? { signal: "cf-ray + " + resp.statusCode, evidence: cfRay.slice(0, 60) } : null
1838
+ },
1839
+ datadome: (resp) => {
1840
+ const dd = headerValue(resp.responseHeaders, "x-dd-b")
1841
+ return dd === "1" ? { signal: "x-dd-b=1", evidence: "" } : null
1842
+ },
1843
+ perimeterx: (resp) => {
1844
+ if (headerValue(resp.responseHeaders, "x-px-block") === "1") {
1845
+ return { signal: "x-px-block=1", evidence: "" }
1846
+ }
1847
+ const pxUuid = headerValue(resp.responseHeaders, "x-px-uuid")
1848
+ if (pxUuid && (resp.statusCode === 403 || resp.statusCode === 429)) {
1849
+ return { signal: "x-px-uuid + " + resp.statusCode, evidence: pxUuid.slice(0, 36) }
1850
+ }
1851
+ return null
1852
+ },
1853
+ imperva: (resp) => {
1854
+ if (resp.statusCode !== 403) return null
1855
+ const iinfo = headerValue(resp.responseHeaders, "x-iinfo")
1856
+ return iinfo ? { signal: "x-iinfo + 403", evidence: iinfo.slice(0, 40) } : null
1857
+ },
1858
+ }
1859
+
1860
+ function headerValue(headers, name) {
1861
+ if (!Array.isArray(headers)) return undefined
1862
+ const lower = name.toLowerCase()
1863
+ for (const h of headers) {
1864
+ if (h.name && h.name.toLowerCase() === lower) return h.value
1865
+ }
1866
+ return undefined
1867
+ }
1868
+
1869
+ // Per-tab deduplication: a single vendor's signature firing repeatedly
1870
+ // on a tab should emit ONE control frame, not one per response. Bridge
1871
+ // already de-dupes by tabId on its side; we de-dupe here too to keep
1872
+ // the wire quiet.
1873
+ const detectedVendorsByTab = new Map() // tabId -> Set<vendor>
1874
+
1875
+ function emitBotDetected(tabId, vendor, signal, evidence) {
1876
+ if (typeof tabId !== "number" || tabId < 0) return
1877
+ if (!nativePort) return
1878
+ let seen = detectedVendorsByTab.get(tabId)
1879
+ if (!seen) {
1880
+ seen = new Set()
1881
+ detectedVendorsByTab.set(tabId, seen)
1882
+ }
1883
+ if (seen.has(vendor)) return
1884
+ seen.add(vendor)
1885
+ try {
1886
+ nativePort.postMessage({
1887
+ type: "__botDetected__",
1888
+ tabId,
1889
+ vendor,
1890
+ signal,
1891
+ evidence,
1892
+ ts: Date.now(),
1893
+ })
1894
+ } catch (err) {
1895
+ console.warn("[browser-bridge/bot-detect] post failed:", err)
1896
+ }
1897
+ }
1898
+
1899
+ // MAIN frame only — sub-resource 403s on tracking pixels are common
1900
+ // noise. Vendor blocks always land on the main document request.
1901
+ try {
1902
+ chrome.webRequest.onHeadersReceived.addListener(
1903
+ (details) => {
1904
+ try {
1905
+ for (const [vendor, probe] of Object.entries(BOT_DETECTION_VENDORS)) {
1906
+ const hit = probe(details)
1907
+ if (hit) {
1908
+ emitBotDetected(details.tabId, vendor, hit.signal, hit.evidence)
1909
+ break
1910
+ }
1911
+ }
1912
+ } catch (err) {
1913
+ console.warn("[browser-bridge/bot-detect] probe crashed:", err)
1914
+ }
1915
+ },
1916
+ { urls: ["<all_urls>"], types: ["main_frame"] },
1917
+ ["responseHeaders"],
1918
+ )
1919
+ } catch (err) {
1920
+ // webRequest permission may not be granted on some enterprise
1921
+ // policies; auto-detect just no-ops in that case.
1922
+ console.warn("[browser-bridge/bot-detect] webRequest listener registration failed:", err)
1923
+ }
1924
+
1925
+ // Cleanup: clear vendor dedup state on navigation + tab close so a
1926
+ // new document gets a fresh detection window.
1927
+ chrome.webNavigation.onCommitted.addListener((details) => {
1928
+ if (details.frameId === 0 && typeof details.tabId === "number") {
1929
+ detectedVendorsByTab.delete(details.tabId)
1930
+ }
1931
+ })
1932
+ chrome.tabs.onRemoved.addListener((tabId) => {
1933
+ detectedVendorsByTab.delete(tabId)
1934
+ })
1935
+
1611
1936
  const TOOL_HANDLERS = {
1612
1937
  __ping__: () => ({
1613
1938
  pong: true,
@@ -1725,11 +2050,43 @@ async function handleBridgeRequest(req, port) {
1725
2050
  try {
1726
2051
  const data = await handler(req.args || {})
1727
2052
  port.postMessage({ id: req.id, ok: true, data })
2053
+ // Snapshot cache invalidation for mutating actions. The matcher
2054
+ // cascade (Phase 2) dispatches against cached snapshots; a
2055
+ // successful click / fill / type / etc. likely changed the page,
2056
+ // so the cached element list is stale. Invalidate by tabId from
2057
+ // the request args; tools that don't carry a tabId (open_tab on
2058
+ // create-path, list_tabs) are not page-mutating per-tab so they
2059
+ // skip this.
2060
+ if (MUTATES_PAGE.has(req.tool)) {
2061
+ const tabId = typeof req.args?.tabId === "number" ? req.args.tabId : undefined
2062
+ if (typeof tabId === "number") {
2063
+ invalidateSnapshot(tabId, `mutation:${req.tool}`)
2064
+ }
2065
+ }
1728
2066
  } catch (err) {
1729
2067
  port.postMessage({ id: req.id, ok: false, error: err && err.message ? err.message : String(err) })
1730
2068
  }
1731
2069
  }
1732
2070
 
2071
+ // Tools whose successful execution likely mutates the page's DOM,
2072
+ // triggering snapshot-cache invalidation for the tabId in args. Kept
2073
+ // as a Set rather than per-tool flags so adding a new mutating tool
2074
+ // is one line. Conservative: tools listed here MAY not mutate (e.g.
2075
+ // click on a disabled button is a no-op); the cost of a spurious
2076
+ // invalidate is one extra capture on next read, vs the cost of a
2077
+ // stale snapshot which is silent dispatch against a vanished ref.
2078
+ const MUTATES_PAGE = new Set([
2079
+ "browser_click",
2080
+ "browser_fill",
2081
+ "browser_type",
2082
+ "browser_keyboard",
2083
+ "browser_scroll",
2084
+ "browser_mouse",
2085
+ "browser_drag",
2086
+ "browser_navigate",
2087
+ "browser_eval_js",
2088
+ ])
2089
+
1733
2090
  chrome.runtime.onInstalled.addListener(() => {
1734
2091
  try { connectBridge() } catch (err) { console.warn("[browser-bridge] onInstalled connect failed:", err) }
1735
2092
  })
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "github-router browser bridge",
4
4
  "short_name": "gh-router-browser",
5
- "version": "0.3.52",
5
+ "version": "0.3.66",
6
6
  "description": "Bridge between Claude (via github-router /mcp) and the browser. Implements tab control, navigation, clicks, form fill, downloads, screenshots, devtools eval. Blocks navigation to chrome://settings.",
7
7
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJElxuBlonBS3TVW9FJN0mGTtShB3L1hoaYf6k39SOr1ogGYmF90EjRxy1i21k9wQQjPf26bcBu/9X67KrQjQV0uB38CaNukgiSeoLjfptN811u+PJHx6BP+jx3Qa6/3VenNPxHC8WEU0GXql8QSjIHEyCwKb6fMASXOK94JyB5Ywov2x8mt/+9ncqBBBMVzf6r5Sagy4PL1XnryLsuADD/vOEkPet8wXgH/Oj7v5tTsQQZ7U1JT51PoDs2BFnXc5v3TkVgZwd32k3ONh+nkDw1Hof+4zwUGOyJE6eMrlYzRlKM4Qxdf9JpavQvqfieAbTRWcyKeclnHeoIfE7cDBQIDAQAB",
8
8
  "background": {
@@ -16,6 +16,7 @@
16
16
  "scripting",
17
17
  "downloads",
18
18
  "webNavigation",
19
+ "webRequest",
19
20
  "debugger",
20
21
  "storage",
21
22
  "alarms"