vivekchakraverty Claude Opus 4.8 commited on
Commit
6b506cb
Β·
1 Parent(s): 1ea78e7

Capture PO token via in-page fetch/XHR hook (fix: token not captured)

Browse files

YouTube fetches media via fetch()/SABR, which the MV3 service-worker webRequest path can
miss. Add MAIN-world content_hook.js that patches fetch + XHR to read the gvs `pot` from
videoplayback URLs (and visitor_data from ytcfg), bridged to session storage via
content_bridge.js. Background now raises session-storage access level for content scripts
and keeps a broadened webRequest capture as backup. Popup gives a precise diagnostic
(media seen vs. token present). Bump to 1.1.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

extension/README.md CHANGED
@@ -52,6 +52,21 @@ It just surfaces these for one-click copy; it sends nothing anywhere.
52
  events so Gradio's Svelte bindings register the change. Requires the `*://*.hf.space/*`
53
  host permission.
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  ## Caveats
56
 
57
  - **Unpublished / sideloaded** β€” you load it yourself; trust accordingly. The code is
 
52
  events so Gradio's Svelte bindings register the change. Requires the `*://*.hf.space/*`
53
  host permission.
54
 
55
+ ## Troubleshooting
56
+
57
+ **"No PO token yet" even though the video is playing.** YouTube fetches media via
58
+ `fetch`/XHR (SABR), so the token is captured by a page-level hook (`content_hook.js`), not
59
+ only the service worker. If it still doesn't appear:
60
+
61
+ - **Reload the video page** after installing the extension (the hook must be in place
62
+ *before* the player starts).
63
+ - Press **play** and let it **buffer a few seconds**, then click **Refresh** in the popup.
64
+ - If the popup says *"Media requests were seen but none carried a PO token,"* your current
65
+ session isn't attaching one β€” try a **logged-out Incognito** window (with the extension
66
+ allowed in Incognito), or just use **cookies** instead.
67
+ - Make sure the extension is **allowed in Incognito** if you test there
68
+ (`chrome://extensions` β†’ Details β†’ Allow in Incognito).
69
+
70
  ## Caveats
71
 
72
  - **Unpublished / sideloaded** β€” you load it yourself; trust accordingly. The code is
extension/background.js CHANGED
@@ -1,27 +1,28 @@
1
- // Service worker: watch the player's videoplayback requests and capture the gvs PO token.
2
- //
3
- // YouTube's web player fetches video data from *.googlevideo.com/videoplayback?...&pot=...
4
- // where `pot` is the gvs Proof-of-Origin token and `c` is the client (e.g. WEB). We read
5
- // it (read-only; no blocking) and stash the latest into session storage for the popup.
6
 
7
- const FILTER = { urls: ["*://*.googlevideo.com/videoplayback*"] };
 
 
 
 
8
 
9
  chrome.webRequest.onBeforeRequest.addListener(
10
  (details) => {
11
  try {
 
 
12
  const u = new URL(details.url);
13
  const pot = u.searchParams.get("pot");
14
  if (!pot) return;
15
  const client = u.searchParams.get("c") || "";
16
- // Prefer WEB tokens (what yt-dlp's web client needs); still record others as backup.
17
- chrome.storage.session.get(["gvsPot", "gvsClient"]).then((cur) => {
18
  const haveWeb = cur.gvsClient === "WEB";
19
  if (!haveWeb || client === "WEB") {
20
- chrome.storage.session.set({
21
- gvsPot: pot,
22
- gvsClient: client,
23
- potTime: Date.now(),
24
- });
25
  }
26
  });
27
  } catch (e) {
 
1
+ // Service worker. Two jobs:
2
+ // 1. Raise the session-storage access level so the youtube.com content-script bridge
3
+ // (an "untrusted" context) can persist captured values for the popup to read.
4
+ // 2. A secondary, read-only webRequest capture of the gvs PO token from
5
+ // *.googlevideo.com/videoplayback requests (the page-level fetch/XHR hook is primary).
6
 
7
+ try {
8
+ chrome.storage.session.setAccessLevel({ accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS" });
9
+ } catch (e) {}
10
+
11
+ const FILTER = { urls: ["*://*.googlevideo.com/*"] };
12
 
13
  chrome.webRequest.onBeforeRequest.addListener(
14
  (details) => {
15
  try {
16
+ if (details.url.indexOf("videoplayback") === -1) return;
17
+ chrome.storage.session.set({ gvAnySeen: true, lastSeen: Date.now() });
18
  const u = new URL(details.url);
19
  const pot = u.searchParams.get("pot");
20
  if (!pot) return;
21
  const client = u.searchParams.get("c") || "";
22
+ chrome.storage.session.get(["gvsClient"]).then((cur) => {
 
23
  const haveWeb = cur.gvsClient === "WEB";
24
  if (!haveWeb || client === "WEB") {
25
+ chrome.storage.session.set({ gvsPot: pot, gvsClient: client, potTime: Date.now() });
 
 
 
 
26
  }
27
  });
28
  } catch (e) {
extension/content_bridge.js ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Isolated-world bridge: receives postMessages from the MAIN-world hook and persists the
2
+ // captured values to session storage (which the popup reads). Needs the background script
3
+ // to have raised the session-storage access level for untrusted contexts.
4
+ window.addEventListener("message", function (e) {
5
+ if (e.source !== window) return;
6
+ const d = e.data;
7
+ if (!d || d.__potgrab !== true) return;
8
+
9
+ if (d.type === "seen") {
10
+ chrome.storage.session.set({ gvAnySeen: true, lastSeen: Date.now() });
11
+ } else if (d.type === "pot" && d.data && d.data.pot) {
12
+ chrome.storage.session.get(["gvsClient"]).then(function (cur) {
13
+ const haveWeb = cur.gvsClient === "WEB";
14
+ if (!haveWeb || d.data.c === "WEB") {
15
+ chrome.storage.session.set({
16
+ gvsPot: d.data.pot,
17
+ gvsClient: d.data.c || "",
18
+ potTime: Date.now(),
19
+ });
20
+ }
21
+ });
22
+ } else if (d.type === "visitor" && d.data && d.data.v) {
23
+ chrome.storage.session.set({ visitorData: d.data.v });
24
+ }
25
+ });
extension/content_hook.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // MAIN-world hook (runs in the page's JS context at document_start).
2
+ //
3
+ // Modern YouTube fetches media via fetch()/XHR to *.googlevideo.com/videoplayback?...&pot=...
4
+ // (the MV3 service-worker webRequest path can miss these). We patch fetch + XHR to observe
5
+ // those request URLs, pull the gvs `pot`, and post it to the isolated bridge script. We
6
+ // also report visitor_data once it's available. We only observe β€” requests pass through
7
+ // untouched.
8
+ (function () {
9
+ function report(type, data) {
10
+ try { window.postMessage({ __potgrab: true, type: type, data: data }, "*"); } catch (e) {}
11
+ }
12
+
13
+ function scanUrl(url) {
14
+ try {
15
+ if (!url || String(url).indexOf("videoplayback") === -1) return;
16
+ report("seen", null);
17
+ const u = new URL(url, location.href);
18
+ const pot = u.searchParams.get("pot");
19
+ if (pot) report("pot", { pot: pot, c: u.searchParams.get("c") || "" });
20
+ } catch (e) {}
21
+ }
22
+
23
+ try {
24
+ const origFetch = window.fetch;
25
+ if (origFetch) {
26
+ window.fetch = function (input) {
27
+ try { scanUrl(typeof input === "string" ? input : (input && input.url)); } catch (e) {}
28
+ return origFetch.apply(this, arguments);
29
+ };
30
+ }
31
+ } catch (e) {}
32
+
33
+ try {
34
+ const origOpen = XMLHttpRequest.prototype.open;
35
+ XMLHttpRequest.prototype.open = function (method, url) {
36
+ try { scanUrl(url); } catch (e) {}
37
+ return origOpen.apply(this, arguments);
38
+ };
39
+ } catch (e) {}
40
+
41
+ function reportVisitor() {
42
+ try {
43
+ if (window.ytcfg && typeof ytcfg.get === "function") {
44
+ const v = ytcfg.get("VISITOR_DATA");
45
+ if (v) { report("visitor", { v: v }); return true; }
46
+ }
47
+ } catch (e) {}
48
+ return false;
49
+ }
50
+ if (!reportVisitor()) {
51
+ let tries = 0;
52
+ const iv = setInterval(function () {
53
+ if (reportVisitor() || ++tries > 30) clearInterval(iv);
54
+ }, 1000);
55
+ }
56
+ })();
extension/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "manifest_version": 3,
3
  "name": "TutorialMaker PO Token Grabber",
4
- "version": "1.0.0",
5
  "description": "Reads your YouTube visitor data and gvs PO token so you can paste them into the TutorialMaker Space.",
6
  "permissions": ["webRequest", "storage", "scripting", "tabs"],
7
  "host_permissions": [
@@ -10,6 +10,21 @@
10
  "*://*.hf.space/*"
11
  ],
12
  "background": { "service_worker": "background.js" },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  "action": {
14
  "default_popup": "popup.html",
15
  "default_title": "PO Token Grabber"
 
1
  {
2
  "manifest_version": 3,
3
  "name": "TutorialMaker PO Token Grabber",
4
+ "version": "1.1.0",
5
  "description": "Reads your YouTube visitor data and gvs PO token so you can paste them into the TutorialMaker Space.",
6
  "permissions": ["webRequest", "storage", "scripting", "tabs"],
7
  "host_permissions": [
 
10
  "*://*.hf.space/*"
11
  ],
12
  "background": { "service_worker": "background.js" },
13
+ "content_scripts": [
14
+ {
15
+ "matches": ["*://*.youtube.com/*"],
16
+ "js": ["content_hook.js"],
17
+ "run_at": "document_start",
18
+ "world": "MAIN",
19
+ "all_frames": true
20
+ },
21
+ {
22
+ "matches": ["*://*.youtube.com/*"],
23
+ "js": ["content_bridge.js"],
24
+ "run_at": "document_start",
25
+ "all_frames": true
26
+ }
27
+ ],
28
  "action": {
29
  "default_popup": "popup.html",
30
  "default_title": "PO Token Grabber"
extension/popup.js CHANGED
@@ -88,6 +88,7 @@ async function load() {
88
  "gvsClient",
89
  "potTime",
90
  "visitorData",
 
91
  ]);
92
  const visitor = liveVisitor || store.visitorData || "";
93
 
@@ -100,11 +101,18 @@ async function load() {
100
  document.getElementById("fillSpace").disabled = spaceTabs.length === 0;
101
 
102
  const notes = [];
103
- if (!isYouTube(tab && tab.url) && !store.gvsPot) {
104
- notes.push("Open a YouTube video, press play, then reopen this popup.");
105
- } else {
106
- if (!visitor) notes.push("No visitor data yet β€” open the popup once on the YouTube tab.");
107
- if (!store.gvsPot) notes.push("No PO token yet β€” press play on a video, then reopen.");
 
 
 
 
 
 
 
108
  }
109
  if (store.gvsPot && store.gvsClient && store.gvsClient !== "WEB") {
110
  notes.push("Captured client is '" + store.gvsClient + "', not WEB β€” use the normal web player.");
 
88
  "gvsClient",
89
  "potTime",
90
  "visitorData",
91
+ "gvAnySeen",
92
  ]);
93
  const visitor = liveVisitor || store.visitorData || "";
94
 
 
101
  document.getElementById("fillSpace").disabled = spaceTabs.length === 0;
102
 
103
  const notes = [];
104
+ if (!visitor) notes.push("No visitor data yet β€” open the popup once on the YouTube tab.");
105
+ if (!store.gvsPot) {
106
+ if (store.gvAnySeen) {
107
+ notes.push("Media requests were seen but none carried a PO token β€” this session may "
108
+ + "not attach one. Try a logged-out Incognito window, reload the video, or use "
109
+ + "cookies instead.");
110
+ } else if (isYouTube(tab && tab.url)) {
111
+ notes.push("No media captured yet β€” press play and let the video buffer a few "
112
+ + "seconds, then Refresh. If a video is already playing, reload the page.");
113
+ } else {
114
+ notes.push("Open a YouTube video and press play to capture a PO token.");
115
+ }
116
  }
117
  if (store.gvsPot && store.gvsClient && store.gvsClient !== "WEB") {
118
  notes.push("Captured client is '" + store.gvsClient + "', not WEB β€” use the normal web player.");