DD8777 Claude Opus 4.8 commited on
Commit
f8b042d
·
1 Parent(s): 939831a

Fix YouTube playback: let Home Relay use fresh Firefox cookies; drop leaked secrets

Browse files

Root cause of "no format" / import-cookies error despite Firefox auto-load:
resolvePlayer did `cookieHeader || firefoxCookiesStr`, so any cookie forwarded by
the Worker (always non-empty via CONSENT/SOCS) shadowed the freshly auto-loaded
Firefox session entirely, giving stale auth and LOGIN_REQUIRED.

- home-relay/server.js: merge forwarded cookies with the Firefox session, Firefox
last so slimCookieHeader keeps its fresher value per cookie name.
- worker.js: empty BUILTIN_YOUTUBE_COOKIE; it held a leaked, expired personal
Google session that both leaked credentials and shadowed the relay cookies.
- start-relay.ps1 / home-relay/run-relay.bat: read RELAY_SECRET from the
environment (generate and persist on first run) instead of hardcoding it.

Also adds relay/autostart tooling, docs, and the localtunnel devDependency.

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

DIAGNOSE-RELAY.md ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # אבחון Relay / Tunnel (502 + health תקוע)
2
+
3
+ ## מה מצאנו (סיבת השורש) — מאומת בלוגים
4
+
5
+ | תסמין | סיבה |
6
+ |--------|------|
7
+ | localtunnel 502 | השרת לא מגיע / מנהרה מתה / **URL לא תואם** |
8
+ | Worker מצביע על `etrog-bypass-mirik-999.loca.lt` | בלוג בפועל נוצר `https://weak-crab-83.loca.lt` — subdomain קבוע **לא נשמר** |
9
+ | `127.0.0.1:8788/health` timeout | תהליך Node **לא רץ** — connection refused (לא IPv6) |
10
+ | cloudflared 502 | `Unable to reach origin … 127.0.0.1:8788 … refused` = **relay down**, לא באג tunnel |
11
+ | LOGIN_REQUIRED מ-CF | צפוי — בלי relay חי אין resolve |
12
+
13
+ **אימות:** אחרי restart של Node, `cloudflared --protocol http2` + `/health` הציבורי החזיר `ok: true`.
14
+
15
+ **localtunnel** בשירות החינמי:
16
+ - מתעלם לעיתים מ-subdomain ומחזיר שם אקראי
17
+ - דורש לעיתים דף "tunnel password" בדפדפן (Worker לא עובר אותו)
18
+ - 502 נפוץ מאוד
19
+
20
+ **הפתרון:** `cloudflared` quick tunnel + עדכון `YT_RELAY_URL` ל-URL **האמיתי** שנוצר.
21
+
22
+ ---
23
+
24
+ ## הפעלה נכונה (Windows)
25
+
26
+ ```powershell
27
+ cd "C:\Users\user\Documents\מעקף פרוקסי 2.0"
28
+
29
+ # טוקן CF — רק מקומית, לא בצ'אט
30
+ $env:CLOUDFLARE_API_TOKEN = "YOUR_NEW_TOKEN"
31
+
32
+ # אופציונלי — אחרת משתמשים בברירת המחדל בסקריפט
33
+ # $env:RELAY_SECRET = "same-secret-as-worker"
34
+
35
+ .\stop-relay.ps1
36
+ .\start-relay.ps1
37
+ ```
38
+
39
+ בדיקות:
40
+
41
+ ```powershell
42
+ Invoke-RestMethod http://127.0.0.1:8788/health
43
+ Get-Content $env:TEMP\yt-relay-url.txt
44
+ $url = (Get-Content $env:TEMP\yt-relay-url.txt).Trim()
45
+ Invoke-RestMethod "$url/health"
46
+ ```
47
+
48
+ Player:
49
+
50
+ ```text
51
+ https://etrog-bypass-proxy.miriklain616.workers.dev/ytp
52
+ ```
53
+
54
+ ---
55
+
56
+ ## IPv4 / IPv6
57
+
58
+ - השרת מאזין על `127.0.0.1` (IPv4 loopback) — זה מה ש-cloudflared מצפה.
59
+ - אין צורך ב-`allow_invalid_cert` ל-localtunnel אם לא משתמשים בו.
60
+ - אם health נכשל: `netstat -ano | findstr 8788` — חייב LISTENING.
61
+
62
+ ---
63
+
64
+ ## cloudflared מול localtunnel
65
+
66
+ | | localtunnel | cloudflared |
67
+ |--|-------------|-------------|
68
+ | יציבות | גרועה | טובה ל-quick tunnel |
69
+ | URL קבוע | כמעט לא | named tunnel + דומיין |
70
+ | סיסמת דף | לפעמים | לא |
71
+ | מומלץ לפרויקט | לא | **כן** |
72
+
73
+ `http2: stream closed` ב-cloudflared לרוב = תהליך local מת / sleep / restart באמצע סטרים. שמור PC ער; לנגן מחדש אחרי reconnect.
74
+
75
+ ---
76
+
77
+ ## Etrog `RWC_BLOCK` / `lighttpd` ב-SW
78
+
79
+ ה-SW בודק תשובות ל-`/assets/chunk-*` וכו'. אם **אין** `X-Etrog-Mode` / seal וה-filter שם `server: lighttpd` — מחזירים 502.
80
+
81
+ זה אומר שהבקשה **לא** עברה דרך ה-Worker (או ש-MITM החליף את הגוף).
82
+ כשה-relay + worker מסמנים `X-Etrog-Mode: stream-unsealed` או sealed — הבדיקה אמורה לדלג.
83
+
84
+ ---
85
+
86
+ ## אבטחה
87
+
88
+ - **בטל** כל Cloudflare API token שנשלח בצ'אט.
89
+ - אל תשים טוקנים ב-`start-relay.ps1` (תוקן).
90
+ - סובב גם `RELAY_SECRET` אם דלף.
RESTRICTED-MODE.md ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Restricted Mode / Bot check / MRM — מדריך מעשי
2
+
3
+ ## שלוש שכבות חסימה שונות (לא לבלבל)
4
+
5
+ | שכבה | הודעה טיפוסית | מי חוסם | פתרון |
6
+ |------|----------------|---------|--------|
7
+ | **A. Cloudflare IP** | LOGIN_REQUIRED מ-Worker | YouTube מול DC | Home Relay (כבר יש) |
8
+ | **B. Bot / age / login** | "צריך להיכנס כדי לאמת שלא מדובר בבוט" | YouTube (חשבון/סשן) | עוגיות + SAPISIDHASH |
9
+ | **C. Network Restricted Mode** | MRM / VIDEO_BLOCK_BY_MRM / מצב מוגן | אתרוג↔Google על ה-IP | חשבון Google בלי Restricted, או egress "נקי" |
10
+
11
+ הרילאי הביתי פותר **A**.
12
+ **B** דורש סשן YouTube אמיתי ב-Firefox + הזרקה נכונה (עודכן ב-`server.js`).
13
+ **C** לא נפתר רק ע"י קליינט InnerTube אחר — Google רואה את ה-IP/הרשת המסוננת.
14
+
15
+ ---
16
+
17
+ ## מה תוקן ב-relay (B)
18
+
19
+ 1. עוגיות מ-Firefox גם מ-`.google.com` (SAPISID/SID)
20
+ 2. `SAPISIDHASH` על WEB/MWEB
21
+ 3. עוגיות גם ל-ANDROID (לא רק WEB)
22
+ 4. רענון עוגיות כל 15 דקות
23
+ 5. `RELAY_INSECURE_TLS=1` אם Node נכשל בגלל תעודת MITM של אתרוג
24
+
25
+ ### בדיקה ידנית אחרי restart
26
+
27
+ ```powershell
28
+ # ודא שיש session
29
+ Invoke-RestMethod http://127.0.0.1:8788/health
30
+ # בלוג: SAPISID=yes, session=yes
31
+
32
+ # נגן סרטון "בעייתי"
33
+ $secret = "YOUR_RELAY_SECRET"
34
+ Invoke-RestMethod -Method POST http://127.0.0.1:8788/v1/yt/player `
35
+ -Headers @{ Authorization = "Bearer $secret" } `
36
+ -ContentType "application/json" `
37
+ -Body '{"videoId":"VIDEO_ID"}' | ConvertTo-Json -Depth 5
38
+ ```
39
+
40
+ חפש ב-`attempts`: `WEB+ck` / `status: OK`.
41
+
42
+ ### אם עדיין LOGIN_REQUIRED
43
+
44
+ 1. בפיירפוקס: היכנס ל-YouTube, נגן סרטון אחד **ישירות** (אם האתר פתוח ברשת)
45
+ 2. בחשבון Google:
46
+ https://myaccount.google.com/u/0/restricted-mode
47
+ וגם הגדרות YouTube → Restricted Mode = **Off**
48
+ 3. הפעל מחדש relay (רענון cookies)
49
+ 4. אם `fetch failed` / certificate:
50
+ ```powershell
51
+ [Environment]::SetEnvironmentVariable("RELAY_INSECURE_TLS","1","User")
52
+ ```
53
+ ואז restart
54
+
55
+ ---
56
+
57
+ ## לגבי "איזה client עוקף Restricted Mode?"
58
+
59
+ | Client | מתי עוזר |
60
+ |--------|----------|
61
+ | **WEB + cookies + SAPISIDHASH** | הכי טוב ל-bot check / age (שכבה B) |
62
+ | **TVHTML5_SIMPLY_EMBEDDED** | חלק מהסרטונים בלי login |
63
+ | **ANDROID בלי cookies** | רוב הסרטונים "רגילים"; נכשל על restricted |
64
+ | **ANDROID_EMBEDDED / WEB_CREATOR** | לא קסם מול MRM רשתי |
65
+
66
+ **אין client קסם** שעוקף Restricted Mode ברמת רשת (שכבה C) כש-Google/אתרוג כופים אותו על ה-IP.
67
+
68
+ ### אופציות אמיתיות ל-C (MRM רשתי)
69
+
70
+ 1. **Hotspot סלולרי** לבדיקה — אם שם עובד, זו שכבה C
71
+ 2. **Egress נקי** (VPN resid שאינו מאחורי אתרוג / מחשב אחר / VPS) רק ל-InnerTube+googlevideo
72
+ 3. **חשבון Google** עם Restricted Mode כבוי + cookies (עוזר לחלק מהמקרים שמסומנים כ"מוגן" ברמת חשבון)
73
+ 4. לא לצפות ש-CF Worker יפתור MRM — ה-IP של CF גרוע יותר
74
+
75
+ ---
76
+
77
+ ## Windows autostart (בעיה #2)
78
+
79
+ מומלץ: **Task Scheduler** (לא רק Startup VBS).
80
+
81
+ ```powershell
82
+ cd "C:\Users\user\Documents\מעקף פרוקסי 2.0"
83
+
84
+ # טוקן כבר אמור להיות ב-User env:
85
+ # [Environment]::SetEnvironmentVariable("CLOUDFLARE_API_TOKEN","...","User")
86
+
87
+ .\install-autostart.ps1
88
+ schtasks /Run /TN EtrogYtHomeRelay
89
+ ```
90
+
91
+ למה לא VBS בלבד:
92
+ - נתיב עברית ב-Startup לפעמים נשבר בקידוד
93
+ - אין restart on failure
94
+ - Scheduled Task עם Delay 45s + RestartOnFailure יציב יותר
95
+
96
+ הסר כפילות:
97
+
98
+ ```powershell
99
+ Remove-Item "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\start_bypass_relay.vbs" -ErrorAction SilentlyContinue
100
+ ```
home-relay/README.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # YouTube Home Relay (residential egress)
2
+
3
+ Small Node.js process that runs on your **home PC**. Your Cloudflare Worker calls it for:
4
+
5
+ 1. **InnerTube** `/youtubei/v1/player` (format resolve)
6
+ 2. **googlevideo** byte streams
7
+
8
+ Both use the **same residential IP**, so YouTube stops returning `LOGIN_REQUIRED` / IP-bound 403s that you get from Cloudflare hosting ranges.
9
+
10
+ ```
11
+ Browser ──► CF Worker ──► Home relay (this) ──► YouTube / googlevideo
12
+
13
+ └── other sites still direct from CF (Brave, etc.)
14
+ ```
15
+
16
+ This is the simplest reliable solo-project option. Paid “residential proxy APIs” are optional; your home IP is free and real.
17
+
18
+ ---
19
+
20
+ ## 1. Run the relay on your PC
21
+
22
+ Requirements: **Node 18+**
23
+
24
+ ```powershell
25
+ cd home-relay
26
+ # Generate a secret (keep private — same value goes into the Worker)
27
+ $env:RELAY_SECRET = -join ((48..57 + 65..90 + 97..122 | Get-Random -Count 40 | ForEach-Object { [char]$_ }))
28
+ $env:PORT = "8788"
29
+ npm start
30
+ ```
31
+
32
+ Health check (no auth):
33
+
34
+ ```text
35
+ http://127.0.0.1:8788/health
36
+ ```
37
+
38
+ ---
39
+
40
+ ## 2. Expose it with Cloudflare Tunnel (free, no port-forward)
41
+
42
+ Install [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/), then:
43
+
44
+ ### Quick test (random `*.trycloudflare.com` URL — changes each run)
45
+
46
+ ```powershell
47
+ cloudflared tunnel --url http://127.0.0.1:8788
48
+ ```
49
+
50
+ Copy the `https://….trycloudflare.com` URL → that is `YT_RELAY_URL`.
51
+
52
+ ### Stable setup (recommended)
53
+
54
+ ```powershell
55
+ cloudflared tunnel login
56
+ cloudflared tunnel create etrog-yt-relay
57
+ # route a subdomain you control, e.g. yt-relay.yourdomain.com
58
+ cloudflared tunnel route dns etrog-yt-relay yt-relay.yourdomain.com
59
+ ```
60
+
61
+ Config file example (`%USERPROFILE%\.cloudflared\config.yml`):
62
+
63
+ ```yaml
64
+ tunnel: <TUNNEL_ID>
65
+ credentials-file: C:\Users\YOU\.cloudflared\<TUNNEL_ID>.json
66
+
67
+ ingress:
68
+ - hostname: yt-relay.yourdomain.com
69
+ service: http://127.0.0.1:8788
70
+ - service: http_status:404
71
+ ```
72
+
73
+ ```powershell
74
+ cloudflared tunnel run etrog-yt-relay
75
+ ```
76
+
77
+ Optional: install as a Windows service so it starts with the PC.
78
+
79
+ ---
80
+
81
+ ## 3. Wire the Worker
82
+
83
+ Set secrets (do **not** commit them):
84
+
85
+ ```powershell
86
+ npx wrangler secret put YT_RELAY_URL
87
+ # paste: https://yt-relay.yourdomain.com (no trailing slash)
88
+
89
+ npx wrangler secret put YT_RELAY_SECRET
90
+ # paste: same value as RELAY_SECRET on the PC
91
+ ```
92
+
93
+ In `wrangler.toml` you only need the names documented; values come from secrets.
94
+
95
+ Worker integration (minimal) — see [WORKER_HOOK.md](./WORKER_HOOK.md).
96
+
97
+ ---
98
+
99
+ ## 4. API (for debugging)
100
+
101
+ All routes except `/health` require:
102
+
103
+ ```http
104
+ Authorization: Bearer <RELAY_SECRET>
105
+ ```
106
+
107
+ ### Resolve formats (home IP)
108
+
109
+ ```powershell
110
+ curl -s -X POST "https://YOUR-TUNNEL/v1/yt/player" `
111
+ -H "Authorization: Bearer $env:RELAY_SECRET" `
112
+ -H "Content-Type: application/json" `
113
+ -d "{\"videoId\":\"dQw4w9WgXcQ\"}"
114
+ ```
115
+
116
+ ### Stream media (same IP as resolve)
117
+
118
+ ```powershell
119
+ # After you have a googlevideo URL from the player response:
120
+ curl -L "https://YOUR-TUNNEL/v1/fetch?url=$( [uri]::EscapeDataString($gvUrl) )" `
121
+ -H "Authorization: Bearer $env:RELAY_SECRET" `
122
+ -o test.mp4
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Security notes
128
+
129
+ | Control | Why |
130
+ |--------|-----|
131
+ | Shared secret | Only your Worker can use the relay |
132
+ | Host allowlist | Blocks SSRF (only YouTube-related hosts) |
133
+ | Bind `127.0.0.1` | Local only; tunnel is the public front |
134
+ | Tunnel HTTPS | Encryption in transit |
135
+
136
+ Anyone with the secret can use **your home IP** for YouTube traffic — treat it like a password. Rotate if it leaks.
137
+
138
+ This relay is for **your own lawful access** through over-blocking filters, same as the main proxy.
139
+
140
+ ---
141
+
142
+ ## Ops tips
143
+
144
+ - PC must be **awake** (disable sleep while using YouTube through the proxy).
145
+ - If formats work but media 403s, you resolved on relay but streamed from CF — both must go through the relay.
146
+ - Cookies (optional): POST `{ "videoId":"…", "cookie":"…" }` to `/v1/yt/player`. Prefer cookies **on the home machine**, not CF.
147
+ - Check public egress IP: open `https://api.ipify.org` from a quick script on the PC and confirm it is residential.
148
+
149
+ ---
150
+
151
+ ## Difficulty
152
+
153
+ | Piece | Effort |
154
+ |-------|--------|
155
+ | This Node server | Done (copy folder, `npm start`) |
156
+ | Quick cloudflared URL | ~5 minutes |
157
+ | Named tunnel + DNS | ~15–30 minutes once |
158
+ | Worker hooks | ~30–60 minutes (see WORKER_HOOK.md) |
159
+
160
+ No extra paid services required.
home-relay/WORKER_HOOK.md ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Worker integration hooks
2
+
3
+ Minimal changes so `/ytp` media uses home egress when secrets are set.
4
+
5
+ ## Env secrets
6
+
7
+ | Name | Example |
8
+ |------|---------|
9
+ | `YT_RELAY_URL` | `https://yt-relay.example.com` |
10
+ | `YT_RELAY_SECRET` | same as PC `RELAY_SECRET` |
11
+
12
+ Export in Worker:
13
+
14
+ ```js
15
+ export default {
16
+ async fetch(request, env, ctx) {
17
+ // pass env into handlers that resolve/stream YouTube
18
+ }
19
+ }
20
+ ```
21
+
22
+ If your `worker.js` still uses the classic `addEventListener('fetch')` form without `env`, switch to module syntax **or** hardcode temporarily only for local tests (never commit secrets).
23
+
24
+ With Wrangler module Worker, secrets appear as `env.YT_RELAY_URL`.
25
+
26
+ ---
27
+
28
+ ## Drop-in helpers (paste near top of worker.js)
29
+
30
+ ```js
31
+ function relayConfigured(env) {
32
+ return !!(env && env.YT_RELAY_URL && env.YT_RELAY_SECRET);
33
+ }
34
+
35
+ function relayBase(env) {
36
+ return String(env.YT_RELAY_URL || '').replace(/\/+$/, '');
37
+ }
38
+
39
+ async function relayPlayer(env, videoId, cookie) {
40
+ const r = await fetch(relayBase(env) + '/v1/yt/player', {
41
+ method: 'POST',
42
+ headers: {
43
+ authorization: 'Bearer ' + env.YT_RELAY_SECRET,
44
+ 'content-type': 'application/json',
45
+ },
46
+ body: JSON.stringify({ videoId, cookie: cookie || '' }),
47
+ });
48
+ const j = await r.json().catch(() => null);
49
+ if (!j || !j.ok || !j.formats || !j.formats.length) {
50
+ return { ok: false, playability: j?.playability, attemptsLog: j?.attempts || [], source: 'home-relay' };
51
+ }
52
+ return {
53
+ ok: true,
54
+ formats: j.formats.map((f) => ({
55
+ itag: f.itag,
56
+ url: f.url,
57
+ mime: (f.mimeType || '').split(';')[0],
58
+ progressive: !!f.progressive,
59
+ hasAudio: !!f.hasAudio,
60
+ hasVideo: !!f.hasVideo,
61
+ bitrate: f.bitrate || 0,
62
+ width: f.width || 0,
63
+ height: f.height || 0,
64
+ })),
65
+ playability: j.playability,
66
+ client: j.client,
67
+ hasCookies: !!cookie,
68
+ hasAuth: !!cookie,
69
+ attemptsLog: j.attempts || [],
70
+ via: 'home-relay',
71
+ };
72
+ }
73
+
74
+ /** Stream googlevideo (or any allowlisted URL) through home IP */
75
+ async function relayFetch(env, targetUrl, init = {}) {
76
+ const u = new URL(relayBase(env) + '/v1/fetch');
77
+ u.searchParams.set('url', targetUrl);
78
+ const headers = {
79
+ authorization: 'Bearer ' + env.YT_RELAY_SECRET,
80
+ };
81
+ if (init.range) headers.range = init.range;
82
+ if (init.userAgent) headers['x-upstream-ua'] = init.userAgent;
83
+ return fetch(u.toString(), { headers, signal: init.signal });
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Where to call them
90
+
91
+ ### 1) Prefer relay in `fetchPlayerFormats` / `getPlayerFormatsCached`
92
+
93
+ At the **start** of format resolve:
94
+
95
+ ```js
96
+ async function fetchPlayerFormats(videoId, request, env) {
97
+ if (relayConfigured(env)) {
98
+ const ytCookies = await getYoutubeCookieHeader(request); // optional
99
+ const pack = await relayPlayer(env, videoId, ytCookies || '');
100
+ if (pack.ok) return pack;
101
+ // fall through to existing CF multi-client path only as last resort
102
+ }
103
+ // … existing tryInnertubeClient races …
104
+ }
105
+ ```
106
+
107
+ ### 2) Prefer relay when fetching media bytes in `handleYtStream`
108
+
109
+ Wherever you currently:
110
+
111
+ ```js
112
+ const mediaRes = await fetch(directMediaUrl, { headers: fetchHeaders, ... });
113
+ ```
114
+
115
+ change to:
116
+
117
+ ```js
118
+ let mediaRes;
119
+ if (relayConfigured(env)) {
120
+ mediaRes = await relayFetch(env, directMediaUrl, {
121
+ range: request.headers.get('range') || undefined,
122
+ userAgent: fetchHeaders['user-agent'],
123
+ });
124
+ } else {
125
+ mediaRes = await fetch(directMediaUrl, { headers: fetchHeaders, redirect: 'follow' });
126
+ }
127
+ ```
128
+
129
+ **Critical:** formats from relay + media from relay (never mix CF media with home-resolved URLs).
130
+
131
+ ### 3) Pass `env` through
132
+
133
+ Today many handlers only take `(request, url)`. Thread `env` from the top-level `fetch(request, env, ctx)`.
134
+
135
+ ---
136
+
137
+ ## Suggested policy once relay is online
138
+
139
+ ```text
140
+ if (YT_RELAY configured):
141
+ resolve on home
142
+ stream on home
143
+ skip CF InnerTube races (saves CPU + avoids false LOGIN_REQUIRED cache)
144
+ else:
145
+ existing behavior (client Origin:null iframe + CF best-effort)
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Smoke test matrix
151
+
152
+ | Test | Expect |
153
+ |------|--------|
154
+ | PC `curl` `/v1/yt/player` | `ok:true`, formats with `https://…googlevideo…` |
155
+ | Worker `/api/health` (optional) | show `relay: up` if you add a check |
156
+ | `/ytp?v=…` play | `X-Etrog-Source` or status text mentioning relay |
157
+ | Kill home relay | Worker falls back or shows clear “egress offline” |
158
+
159
+ Optional health from Worker:
160
+
161
+ ```js
162
+ async function relayHealth(env) {
163
+ try {
164
+ const r = await fetch(relayBase(env) + '/health', { signal: AbortSignal.timeout(4000) });
165
+ return r.ok;
166
+ } catch { return false; }
167
+ }
168
+ ```
169
+
170
+ (Health is public on purpose so you can ping without shipping the secret to browsers.)
home-relay/launch-relay.vbs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Set sh = CreateObject("WScript.Shell")
2
+ Dim relayDir, relayLog, relayErr, secret, nodeExe
3
+ relayDir = WScript.Arguments(0)
4
+ relayLog = WScript.Arguments(1)
5
+ relayErr = WScript.Arguments(2)
6
+ secret = WScript.Arguments(3)
7
+
8
+ sh.Environment("Process")("RELAY_SECRET") = secret
9
+ sh.Environment("Process")("PORT") = "8788"
10
+
11
+ Dim cmd
12
+ cmd = "cmd.exe /c node server.js >> """ & relayLog & """ 2>> """ & relayErr & """"
13
+
14
+ sh.CurrentDirectory = relayDir
15
+ sh.Run cmd, 0, False
home-relay/launch-tunnel.vbs ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ Set sh = CreateObject("WScript.Shell")
2
+ Dim cfExe, cfLog
3
+ cfExe = WScript.Arguments(0)
4
+ cfLog = WScript.Arguments(1)
5
+
6
+ Dim cmd
7
+ cmd = "cmd.exe /c """ & cfExe & """ tunnel --protocol http2 --url http://127.0.0.1:8788 --logfile """ & cfLog & """"
8
+
9
+ sh.Run cmd, 0, False
home-relay/package-lock.json ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "etrog-yt-home-relay",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "etrog-yt-home-relay",
9
+ "version": "1.0.0",
10
+ "dependencies": {
11
+ "sqlite3": "^6.0.1"
12
+ },
13
+ "engines": {
14
+ "node": ">=18"
15
+ }
16
+ },
17
+ "node_modules/@isaacs/fs-minipass": {
18
+ "version": "4.0.1",
19
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
20
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
21
+ "license": "ISC",
22
+ "dependencies": {
23
+ "minipass": "^7.0.4"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ }
28
+ },
29
+ "node_modules/abbrev": {
30
+ "version": "4.0.0",
31
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
32
+ "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==",
33
+ "license": "ISC",
34
+ "optional": true,
35
+ "engines": {
36
+ "node": "^20.17.0 || >=22.9.0"
37
+ }
38
+ },
39
+ "node_modules/base64-js": {
40
+ "version": "1.5.1",
41
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
42
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
43
+ "funding": [
44
+ {
45
+ "type": "github",
46
+ "url": "https://github.com/sponsors/feross"
47
+ },
48
+ {
49
+ "type": "patreon",
50
+ "url": "https://www.patreon.com/feross"
51
+ },
52
+ {
53
+ "type": "consulting",
54
+ "url": "https://feross.org/support"
55
+ }
56
+ ],
57
+ "license": "MIT"
58
+ },
59
+ "node_modules/bindings": {
60
+ "version": "1.5.0",
61
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
62
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
63
+ "license": "MIT",
64
+ "dependencies": {
65
+ "file-uri-to-path": "1.0.0"
66
+ }
67
+ },
68
+ "node_modules/bl": {
69
+ "version": "4.1.0",
70
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
71
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
72
+ "license": "MIT",
73
+ "dependencies": {
74
+ "buffer": "^5.5.0",
75
+ "inherits": "^2.0.4",
76
+ "readable-stream": "^3.4.0"
77
+ }
78
+ },
79
+ "node_modules/buffer": {
80
+ "version": "5.7.1",
81
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
82
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
83
+ "funding": [
84
+ {
85
+ "type": "github",
86
+ "url": "https://github.com/sponsors/feross"
87
+ },
88
+ {
89
+ "type": "patreon",
90
+ "url": "https://www.patreon.com/feross"
91
+ },
92
+ {
93
+ "type": "consulting",
94
+ "url": "https://feross.org/support"
95
+ }
96
+ ],
97
+ "license": "MIT",
98
+ "dependencies": {
99
+ "base64-js": "^1.3.1",
100
+ "ieee754": "^1.1.13"
101
+ }
102
+ },
103
+ "node_modules/chownr": {
104
+ "version": "3.0.0",
105
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
106
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
107
+ "license": "BlueOak-1.0.0",
108
+ "engines": {
109
+ "node": ">=18"
110
+ }
111
+ },
112
+ "node_modules/decompress-response": {
113
+ "version": "6.0.0",
114
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
115
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
116
+ "license": "MIT",
117
+ "dependencies": {
118
+ "mimic-response": "^3.1.0"
119
+ },
120
+ "engines": {
121
+ "node": ">=10"
122
+ },
123
+ "funding": {
124
+ "url": "https://github.com/sponsors/sindresorhus"
125
+ }
126
+ },
127
+ "node_modules/deep-extend": {
128
+ "version": "0.6.0",
129
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
130
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
131
+ "license": "MIT",
132
+ "engines": {
133
+ "node": ">=4.0.0"
134
+ }
135
+ },
136
+ "node_modules/detect-libc": {
137
+ "version": "2.1.2",
138
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
139
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
140
+ "license": "Apache-2.0",
141
+ "engines": {
142
+ "node": ">=8"
143
+ }
144
+ },
145
+ "node_modules/end-of-stream": {
146
+ "version": "1.4.5",
147
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
148
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
149
+ "license": "MIT",
150
+ "dependencies": {
151
+ "once": "^1.4.0"
152
+ }
153
+ },
154
+ "node_modules/env-paths": {
155
+ "version": "2.2.1",
156
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
157
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
158
+ "license": "MIT",
159
+ "optional": true,
160
+ "engines": {
161
+ "node": ">=6"
162
+ }
163
+ },
164
+ "node_modules/expand-template": {
165
+ "version": "2.0.3",
166
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
167
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
168
+ "license": "(MIT OR WTFPL)",
169
+ "engines": {
170
+ "node": ">=6"
171
+ }
172
+ },
173
+ "node_modules/exponential-backoff": {
174
+ "version": "3.1.3",
175
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
176
+ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==",
177
+ "license": "Apache-2.0",
178
+ "optional": true
179
+ },
180
+ "node_modules/fdir": {
181
+ "version": "6.5.0",
182
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
183
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
184
+ "license": "MIT",
185
+ "optional": true,
186
+ "engines": {
187
+ "node": ">=12.0.0"
188
+ },
189
+ "peerDependencies": {
190
+ "picomatch": "^3 || ^4"
191
+ },
192
+ "peerDependenciesMeta": {
193
+ "picomatch": {
194
+ "optional": true
195
+ }
196
+ }
197
+ },
198
+ "node_modules/file-uri-to-path": {
199
+ "version": "1.0.0",
200
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
201
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
202
+ "license": "MIT"
203
+ },
204
+ "node_modules/fs-constants": {
205
+ "version": "1.0.0",
206
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
207
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
208
+ "license": "MIT"
209
+ },
210
+ "node_modules/github-from-package": {
211
+ "version": "0.0.0",
212
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
213
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
214
+ "license": "MIT"
215
+ },
216
+ "node_modules/graceful-fs": {
217
+ "version": "4.2.11",
218
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
219
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
220
+ "license": "ISC",
221
+ "optional": true
222
+ },
223
+ "node_modules/ieee754": {
224
+ "version": "1.2.1",
225
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
226
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
227
+ "funding": [
228
+ {
229
+ "type": "github",
230
+ "url": "https://github.com/sponsors/feross"
231
+ },
232
+ {
233
+ "type": "patreon",
234
+ "url": "https://www.patreon.com/feross"
235
+ },
236
+ {
237
+ "type": "consulting",
238
+ "url": "https://feross.org/support"
239
+ }
240
+ ],
241
+ "license": "BSD-3-Clause"
242
+ },
243
+ "node_modules/inherits": {
244
+ "version": "2.0.4",
245
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
246
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
247
+ "license": "ISC"
248
+ },
249
+ "node_modules/ini": {
250
+ "version": "1.3.8",
251
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
252
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
253
+ "license": "ISC"
254
+ },
255
+ "node_modules/isexe": {
256
+ "version": "4.0.0",
257
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz",
258
+ "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==",
259
+ "license": "BlueOak-1.0.0",
260
+ "optional": true,
261
+ "engines": {
262
+ "node": ">=20"
263
+ }
264
+ },
265
+ "node_modules/mimic-response": {
266
+ "version": "3.1.0",
267
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
268
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
269
+ "license": "MIT",
270
+ "engines": {
271
+ "node": ">=10"
272
+ },
273
+ "funding": {
274
+ "url": "https://github.com/sponsors/sindresorhus"
275
+ }
276
+ },
277
+ "node_modules/minimist": {
278
+ "version": "1.2.8",
279
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
280
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
281
+ "license": "MIT",
282
+ "funding": {
283
+ "url": "https://github.com/sponsors/ljharb"
284
+ }
285
+ },
286
+ "node_modules/minipass": {
287
+ "version": "7.1.3",
288
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
289
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
290
+ "license": "BlueOak-1.0.0",
291
+ "engines": {
292
+ "node": ">=16 || 14 >=14.17"
293
+ }
294
+ },
295
+ "node_modules/minizlib": {
296
+ "version": "3.1.0",
297
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
298
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
299
+ "license": "MIT",
300
+ "dependencies": {
301
+ "minipass": "^7.1.2"
302
+ },
303
+ "engines": {
304
+ "node": ">= 18"
305
+ }
306
+ },
307
+ "node_modules/mkdirp-classic": {
308
+ "version": "0.5.3",
309
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
310
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
311
+ "license": "MIT"
312
+ },
313
+ "node_modules/napi-build-utils": {
314
+ "version": "2.0.0",
315
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
316
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
317
+ "license": "MIT"
318
+ },
319
+ "node_modules/node-abi": {
320
+ "version": "3.94.0",
321
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
322
+ "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==",
323
+ "license": "MIT",
324
+ "dependencies": {
325
+ "semver": "^7.3.5"
326
+ },
327
+ "engines": {
328
+ "node": ">=10"
329
+ }
330
+ },
331
+ "node_modules/node-addon-api": {
332
+ "version": "8.9.0",
333
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz",
334
+ "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==",
335
+ "license": "MIT",
336
+ "engines": {
337
+ "node": "^18 || ^20 || >= 21"
338
+ }
339
+ },
340
+ "node_modules/node-gyp": {
341
+ "version": "12.4.0",
342
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz",
343
+ "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==",
344
+ "license": "MIT",
345
+ "optional": true,
346
+ "dependencies": {
347
+ "env-paths": "^2.2.0",
348
+ "exponential-backoff": "^3.1.1",
349
+ "graceful-fs": "^4.2.6",
350
+ "nopt": "^9.0.0",
351
+ "proc-log": "^6.0.0",
352
+ "semver": "^7.3.5",
353
+ "tar": "^7.5.4",
354
+ "tinyglobby": "^0.2.12",
355
+ "undici": "^6.25.0",
356
+ "which": "^6.0.0"
357
+ },
358
+ "bin": {
359
+ "node-gyp": "bin/node-gyp.js"
360
+ },
361
+ "engines": {
362
+ "node": "^20.17.0 || >=22.9.0"
363
+ }
364
+ },
365
+ "node_modules/nopt": {
366
+ "version": "9.0.0",
367
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
368
+ "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==",
369
+ "license": "ISC",
370
+ "optional": true,
371
+ "dependencies": {
372
+ "abbrev": "^4.0.0"
373
+ },
374
+ "bin": {
375
+ "nopt": "bin/nopt.js"
376
+ },
377
+ "engines": {
378
+ "node": "^20.17.0 || >=22.9.0"
379
+ }
380
+ },
381
+ "node_modules/once": {
382
+ "version": "1.4.0",
383
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
384
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
385
+ "license": "ISC",
386
+ "dependencies": {
387
+ "wrappy": "1"
388
+ }
389
+ },
390
+ "node_modules/picomatch": {
391
+ "version": "4.0.5",
392
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
393
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
394
+ "license": "MIT",
395
+ "optional": true,
396
+ "peer": true,
397
+ "engines": {
398
+ "node": ">=12"
399
+ },
400
+ "funding": {
401
+ "url": "https://github.com/sponsors/jonschlinkert"
402
+ }
403
+ },
404
+ "node_modules/prebuild-install": {
405
+ "version": "7.1.3",
406
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
407
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
408
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
409
+ "license": "MIT",
410
+ "dependencies": {
411
+ "detect-libc": "^2.0.0",
412
+ "expand-template": "^2.0.3",
413
+ "github-from-package": "0.0.0",
414
+ "minimist": "^1.2.3",
415
+ "mkdirp-classic": "^0.5.3",
416
+ "napi-build-utils": "^2.0.0",
417
+ "node-abi": "^3.3.0",
418
+ "pump": "^3.0.0",
419
+ "rc": "^1.2.7",
420
+ "simple-get": "^4.0.0",
421
+ "tar-fs": "^2.0.0",
422
+ "tunnel-agent": "^0.6.0"
423
+ },
424
+ "bin": {
425
+ "prebuild-install": "bin.js"
426
+ },
427
+ "engines": {
428
+ "node": ">=10"
429
+ }
430
+ },
431
+ "node_modules/proc-log": {
432
+ "version": "6.1.0",
433
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
434
+ "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==",
435
+ "license": "ISC",
436
+ "optional": true,
437
+ "engines": {
438
+ "node": "^20.17.0 || >=22.9.0"
439
+ }
440
+ },
441
+ "node_modules/pump": {
442
+ "version": "3.0.4",
443
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
444
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
445
+ "license": "MIT",
446
+ "dependencies": {
447
+ "end-of-stream": "^1.1.0",
448
+ "once": "^1.3.1"
449
+ }
450
+ },
451
+ "node_modules/rc": {
452
+ "version": "1.2.8",
453
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
454
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
455
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
456
+ "dependencies": {
457
+ "deep-extend": "^0.6.0",
458
+ "ini": "~1.3.0",
459
+ "minimist": "^1.2.0",
460
+ "strip-json-comments": "~2.0.1"
461
+ },
462
+ "bin": {
463
+ "rc": "cli.js"
464
+ }
465
+ },
466
+ "node_modules/readable-stream": {
467
+ "version": "3.6.2",
468
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
469
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
470
+ "license": "MIT",
471
+ "dependencies": {
472
+ "inherits": "^2.0.3",
473
+ "string_decoder": "^1.1.1",
474
+ "util-deprecate": "^1.0.1"
475
+ },
476
+ "engines": {
477
+ "node": ">= 6"
478
+ }
479
+ },
480
+ "node_modules/safe-buffer": {
481
+ "version": "5.2.1",
482
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
483
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
484
+ "funding": [
485
+ {
486
+ "type": "github",
487
+ "url": "https://github.com/sponsors/feross"
488
+ },
489
+ {
490
+ "type": "patreon",
491
+ "url": "https://www.patreon.com/feross"
492
+ },
493
+ {
494
+ "type": "consulting",
495
+ "url": "https://feross.org/support"
496
+ }
497
+ ],
498
+ "license": "MIT"
499
+ },
500
+ "node_modules/semver": {
501
+ "version": "7.8.5",
502
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
503
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
504
+ "license": "ISC",
505
+ "bin": {
506
+ "semver": "bin/semver.js"
507
+ },
508
+ "engines": {
509
+ "node": ">=10"
510
+ }
511
+ },
512
+ "node_modules/simple-concat": {
513
+ "version": "1.0.1",
514
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
515
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
516
+ "funding": [
517
+ {
518
+ "type": "github",
519
+ "url": "https://github.com/sponsors/feross"
520
+ },
521
+ {
522
+ "type": "patreon",
523
+ "url": "https://www.patreon.com/feross"
524
+ },
525
+ {
526
+ "type": "consulting",
527
+ "url": "https://feross.org/support"
528
+ }
529
+ ],
530
+ "license": "MIT"
531
+ },
532
+ "node_modules/simple-get": {
533
+ "version": "4.0.1",
534
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
535
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
536
+ "funding": [
537
+ {
538
+ "type": "github",
539
+ "url": "https://github.com/sponsors/feross"
540
+ },
541
+ {
542
+ "type": "patreon",
543
+ "url": "https://www.patreon.com/feross"
544
+ },
545
+ {
546
+ "type": "consulting",
547
+ "url": "https://feross.org/support"
548
+ }
549
+ ],
550
+ "license": "MIT",
551
+ "dependencies": {
552
+ "decompress-response": "^6.0.0",
553
+ "once": "^1.3.1",
554
+ "simple-concat": "^1.0.0"
555
+ }
556
+ },
557
+ "node_modules/sqlite3": {
558
+ "version": "6.0.1",
559
+ "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-6.0.1.tgz",
560
+ "integrity": "sha512-X0czUUMG2tmSqJpEQa3tCuZSHKIx8PwM53vLZzKp/o6Rpy25fiVfjdbnZ988M8+O3ZWR1ih0K255VumCb3MAnQ==",
561
+ "hasInstallScript": true,
562
+ "license": "BSD-3-Clause",
563
+ "dependencies": {
564
+ "bindings": "^1.5.0",
565
+ "node-addon-api": "^8.0.0",
566
+ "prebuild-install": "^7.1.3",
567
+ "tar": "^7.5.10"
568
+ },
569
+ "engines": {
570
+ "node": ">=20.17.0"
571
+ },
572
+ "optionalDependencies": {
573
+ "node-gyp": "12.x"
574
+ },
575
+ "peerDependencies": {
576
+ "node-gyp": "12.x"
577
+ },
578
+ "peerDependenciesMeta": {
579
+ "node-gyp": {
580
+ "optional": true
581
+ }
582
+ }
583
+ },
584
+ "node_modules/string_decoder": {
585
+ "version": "1.3.0",
586
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
587
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
588
+ "license": "MIT",
589
+ "dependencies": {
590
+ "safe-buffer": "~5.2.0"
591
+ }
592
+ },
593
+ "node_modules/strip-json-comments": {
594
+ "version": "2.0.1",
595
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
596
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
597
+ "license": "MIT",
598
+ "engines": {
599
+ "node": ">=0.10.0"
600
+ }
601
+ },
602
+ "node_modules/tar": {
603
+ "version": "7.5.20",
604
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.20.tgz",
605
+ "integrity": "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==",
606
+ "license": "BlueOak-1.0.0",
607
+ "dependencies": {
608
+ "@isaacs/fs-minipass": "^4.0.0",
609
+ "chownr": "^3.0.0",
610
+ "minipass": "^7.1.2",
611
+ "minizlib": "^3.1.0",
612
+ "yallist": "^5.0.0"
613
+ },
614
+ "engines": {
615
+ "node": ">=18"
616
+ }
617
+ },
618
+ "node_modules/tar-fs": {
619
+ "version": "2.1.5",
620
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
621
+ "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
622
+ "license": "MIT",
623
+ "dependencies": {
624
+ "chownr": "^1.1.1",
625
+ "mkdirp-classic": "^0.5.2",
626
+ "pump": "^3.0.0",
627
+ "tar-stream": "^2.1.4"
628
+ }
629
+ },
630
+ "node_modules/tar-fs/node_modules/chownr": {
631
+ "version": "1.1.4",
632
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
633
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
634
+ "license": "ISC"
635
+ },
636
+ "node_modules/tar-stream": {
637
+ "version": "2.2.0",
638
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
639
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
640
+ "license": "MIT",
641
+ "dependencies": {
642
+ "bl": "^4.0.3",
643
+ "end-of-stream": "^1.4.1",
644
+ "fs-constants": "^1.0.0",
645
+ "inherits": "^2.0.3",
646
+ "readable-stream": "^3.1.1"
647
+ },
648
+ "engines": {
649
+ "node": ">=6"
650
+ }
651
+ },
652
+ "node_modules/tinyglobby": {
653
+ "version": "0.2.17",
654
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
655
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
656
+ "license": "MIT",
657
+ "optional": true,
658
+ "dependencies": {
659
+ "fdir": "^6.5.0",
660
+ "picomatch": "^4.0.4"
661
+ },
662
+ "engines": {
663
+ "node": ">=12.0.0"
664
+ },
665
+ "funding": {
666
+ "url": "https://github.com/sponsors/SuperchupuDev"
667
+ }
668
+ },
669
+ "node_modules/tunnel-agent": {
670
+ "version": "0.6.0",
671
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
672
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
673
+ "license": "Apache-2.0",
674
+ "dependencies": {
675
+ "safe-buffer": "^5.0.1"
676
+ },
677
+ "engines": {
678
+ "node": "*"
679
+ }
680
+ },
681
+ "node_modules/undici": {
682
+ "version": "6.27.0",
683
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
684
+ "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
685
+ "license": "MIT",
686
+ "optional": true,
687
+ "engines": {
688
+ "node": ">=18.17"
689
+ }
690
+ },
691
+ "node_modules/util-deprecate": {
692
+ "version": "1.0.2",
693
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
694
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
695
+ "license": "MIT"
696
+ },
697
+ "node_modules/which": {
698
+ "version": "6.0.1",
699
+ "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz",
700
+ "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==",
701
+ "license": "ISC",
702
+ "optional": true,
703
+ "dependencies": {
704
+ "isexe": "^4.0.0"
705
+ },
706
+ "bin": {
707
+ "node-which": "bin/which.js"
708
+ },
709
+ "engines": {
710
+ "node": "^20.17.0 || >=22.9.0"
711
+ }
712
+ },
713
+ "node_modules/wrappy": {
714
+ "version": "1.0.2",
715
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
716
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
717
+ "license": "ISC"
718
+ },
719
+ "node_modules/yallist": {
720
+ "version": "5.0.0",
721
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
722
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
723
+ "license": "BlueOak-1.0.0",
724
+ "engines": {
725
+ "node": ">=18"
726
+ }
727
+ }
728
+ }
729
+ }
home-relay/package.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "etrog-yt-home-relay",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Home residential egress for YouTube InnerTube + googlevideo (pairs with etrog-bypass-proxy Worker)",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node server.js",
9
+ "dev": "node --watch server.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "dependencies": {
15
+ "sqlite3": "^6.0.1"
16
+ }
17
+ }
home-relay/run-relay.bat ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ rem RELAY_SECRET must come from the environment (set once via start-relay.ps1 or setx).
3
+ rem Never hardcode it here — this file is committed.
4
+ if "%RELAY_SECRET%"=="" (
5
+ echo ERROR: RELAY_SECRET is not set.
6
+ echo Set it once: setx RELAY_SECRET "your-long-random-secret"
7
+ echo Then open a NEW terminal and re-run this script.
8
+ exit /b 1
9
+ )
10
+ set PORT=8788
11
+ cd /d "%~dp0"
12
+ node server.js >> "%TEMP%\relay.log" 2>> "%TEMP%\relay-error.log"
home-relay/run-tunnel.bat ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ @echo off
2
+ "C:\Program Files (x86)\cloudflared\cloudflared.exe" tunnel --protocol http2 --url http://127.0.0.1:8788 --logfile "%TEMP%\cloudflared-relay.log"
home-relay/server.js ADDED
@@ -0,0 +1,871 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Etrog YT Home Relay — residential egress for YouTube only
3
+ * ─────────────────────────────────────────────────────────
4
+ * Runs on your home PC (real residential IP). Cloudflare Worker calls this
5
+ * for InnerTube /player and googlevideo bytes so both share the SAME IP.
6
+ *
7
+ * Endpoints (all require Authorization: Bearer <RELAY_SECRET>):
8
+ * GET /health → { ok, ip hint }
9
+ * POST /v1/yt/player → { videoId } → formats + playability (from home IP)
10
+ * GET /v1/fetch?url=… → stream arbitrary allowed URL (googlevideo, etc.)
11
+ * POST /v1/fetch → { url, method?, headers?, body? } JSON or stream
12
+ *
13
+ * Quick start:
14
+ * set RELAY_SECRET=long-random-string
15
+ * npm start
16
+ * cloudflared tunnel --url http://127.0.0.1:8788
17
+ * # put the https URL + secret into Worker env (YT_RELAY_URL, YT_RELAY_SECRET)
18
+ */
19
+
20
+ import http from 'node:http';
21
+ import { URL } from 'node:url';
22
+ import { randomUUID, createHash } from 'node:crypto';
23
+ import { Readable } from 'node:stream';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import sqlite3Pkg from 'sqlite3';
27
+ const sqlite3 = sqlite3Pkg.default || sqlite3Pkg;
28
+
29
+ // Always bind loopback — cloudflared/localtunnel connect here.
30
+ // Do NOT use 0.0.0.0 unless you intentionally expose the port on LAN.
31
+ const PORT = Number(process.env.PORT || 8788);
32
+ const HOST = process.env.HOST || '127.0.0.1';
33
+ const RELAY_SECRET = process.env.RELAY_SECRET || '';
34
+ const FETCH_TIMEOUT_MS = Number(process.env.FETCH_TIMEOUT_MS || 25000);
35
+ // Etrog/Rimon MITM: set RELAY_INSECURE_TLS=1 if Node fetch fails with cert errors
36
+ const INSECURE_TLS = process.env.RELAY_INSECURE_TLS === '1' || process.env.RELAY_INSECURE_TLS === 'true';
37
+ if (INSECURE_TLS) {
38
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
39
+ console.warn('[relay] RELAY_INSECURE_TLS=1 — TLS cert verification DISABLED (filter MITM mode)');
40
+ }
41
+
42
+ // Cookies that matter for Innertube auth (keep header small)
43
+ const AUTH_COOKIE_NAMES = new Set([
44
+ 'LOGIN_INFO', 'SID', 'HSID', 'SSID', 'APISID', 'SAPISID',
45
+ '__Secure-1PSID', '__Secure-3PSID', '__Secure-1PAPISID', '__Secure-3PAPISID',
46
+ '__Secure-1PSIDTS', '__Secure-3PSIDTS', '__Secure-1PSIDCC', '__Secure-3PSIDCC',
47
+ 'PREF', 'SOCS', 'VISITOR_INFO1_LIVE', 'YSC', 'CONSENT',
48
+ ]);
49
+
50
+ // Only these host suffixes may be fetched through the relay (SSRF guard)
51
+ const ALLOWED_HOST_SUFFIXES = [
52
+ 'youtube.com',
53
+ 'youtubei.googleapis.com',
54
+ 'googleapis.com',
55
+ 'googlevideo.com',
56
+ 'gvt1.com',
57
+ 'gvt2.com',
58
+ 'ytimg.com',
59
+ 'ggpht.com',
60
+ 'googleusercontent.com',
61
+ ];
62
+
63
+ const INNERTUBE_KEY_ANDROID = 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w';
64
+ const INNERTUBE_KEY_WEB = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
65
+
66
+ // ── helpers ──────────────────────────────────────────────────────────────
67
+
68
+ function log(...args) {
69
+ const ts = new Date().toISOString().slice(11, 19);
70
+ console.log(`[${ts}]`, ...args);
71
+ }
72
+
73
+ function json(res, status, obj) {
74
+ const body = JSON.stringify(obj);
75
+ res.writeHead(status, {
76
+ 'content-type': 'application/json; charset=utf-8',
77
+ 'cache-control': 'no-store',
78
+ 'access-control-allow-origin': '*',
79
+ });
80
+ res.end(body);
81
+ }
82
+
83
+ function unauthorized(res) {
84
+ json(res, 401, { error: 'unauthorized' });
85
+ }
86
+
87
+ function badRequest(res, msg) {
88
+ json(res, 400, { error: msg || 'bad request' });
89
+ }
90
+
91
+ function readBody(req, limit = 2 * 1024 * 1024) {
92
+ return new Promise((resolve, reject) => {
93
+ const chunks = [];
94
+ let size = 0;
95
+ req.on('data', (c) => {
96
+ size += c.length;
97
+ if (size > limit) {
98
+ reject(new Error('body too large'));
99
+ req.destroy();
100
+ return;
101
+ }
102
+ chunks.push(c);
103
+ });
104
+ req.on('end', () => resolve(Buffer.concat(chunks)));
105
+ req.on('error', reject);
106
+ });
107
+ }
108
+
109
+ function checkAuth(req) {
110
+ if (!RELAY_SECRET) return false;
111
+ const h = req.headers.authorization || '';
112
+ if (h === `Bearer ${RELAY_SECRET}`) return true;
113
+ // Also accept header used by some gateways
114
+ if (req.headers['x-relay-secret'] === RELAY_SECRET) return true;
115
+ return false;
116
+ }
117
+
118
+ function hostAllowed(hostname) {
119
+ const h = String(hostname || '').toLowerCase().replace(/\.$/, '');
120
+ if (!h) return false;
121
+ return ALLOWED_HOST_SUFFIXES.some(
122
+ (s) => h === s || h.endsWith('.' + s)
123
+ );
124
+ }
125
+
126
+ function assertAllowedUrl(urlStr) {
127
+ let u;
128
+ try {
129
+ u = new URL(urlStr);
130
+ } catch {
131
+ throw new Error('invalid url');
132
+ }
133
+ if (u.protocol !== 'https:' && u.protocol !== 'http:') {
134
+ throw new Error('only http(s)');
135
+ }
136
+ if (!hostAllowed(u.hostname)) {
137
+ throw new Error('host not allowed: ' + u.hostname);
138
+ }
139
+ return u;
140
+ }
141
+
142
+ /** WEB Innertube expects SAPISIDHASH authorization when cookies are present */
143
+ function computeSapisidHash(cookieStr) {
144
+ try {
145
+ const m = String(cookieStr || '').match(
146
+ /(?:^|;\s*)(?:__Secure-3PAPISID|__Secure-1PAPISID|SAPISID)=([^;]+)/
147
+ );
148
+ if (!m) return null;
149
+ const ts = Math.floor(Date.now() / 1000).toString();
150
+ const origin = 'https://www.youtube.com';
151
+ const hash = createHash('sha1').update(`${ts} ${m[1]} ${origin}`).digest('hex');
152
+ return `SAPISIDHASH ${ts}_${hash}`;
153
+ } catch {
154
+ return null;
155
+ }
156
+ }
157
+
158
+ function slimCookieHeader(cookieStr) {
159
+ if (!cookieStr) return '';
160
+ const map = new Map();
161
+ for (const part of String(cookieStr).split(';')) {
162
+ const eq = part.indexOf('=');
163
+ if (eq === -1) continue;
164
+ const name = part.slice(0, eq).trim();
165
+ const val = part.slice(eq + 1).trim();
166
+ if (!name || !AUTH_COOKIE_NAMES.has(name)) continue;
167
+ // Later hosts overwrite earlier — prefer youtube-specific values already in string order
168
+ map.set(name, val);
169
+ }
170
+ return [...map.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
171
+ }
172
+
173
+ function collectFormats(playerJson) {
174
+ const sd = (playerJson && playerJson.streamingData) || {};
175
+ const raw = [].concat(sd.formats || [], sd.adaptiveFormats || []);
176
+ const out = [];
177
+ for (const f of raw) {
178
+ if (!f) continue;
179
+ // Prefer cipher-free URLs; skip signatureCipher-only (needs nsig on client)
180
+ const url = f.url || null;
181
+ if (!url) continue;
182
+ const mt = String(f.mimeType || '');
183
+ const hasA = !!(
184
+ f.audioQuality ||
185
+ f.audioSampleRate ||
186
+ mt.includes('audio') ||
187
+ f.audioChannels != null
188
+ );
189
+ const hasV = !!(f.width || f.height || mt.includes('video'));
190
+ if (mt.includes('mp4') && f.audioChannels != null) {
191
+ // progressive mp4 often has both
192
+ }
193
+ out.push({
194
+ itag: f.itag,
195
+ url,
196
+ mimeType: mt,
197
+ bitrate: f.bitrate || 0,
198
+ width: f.width || 0,
199
+ height: f.height || 0,
200
+ contentLength: f.contentLength || f.content_length || null,
201
+ hasAudio: hasA,
202
+ hasVideo: hasV,
203
+ progressive: !!(hasA && hasV),
204
+ });
205
+ }
206
+ return out;
207
+ }
208
+
209
+ async function tryPlayerClient(videoId, cl, workerUrl = null) {
210
+ if (workerUrl) {
211
+ const ac = new AbortController();
212
+ const t = setTimeout(() => ac.abort(), cl.timeoutMs || 12000);
213
+ try {
214
+ const helperEndpoint = workerUrl.replace(/\/+$/, '') + '/api/resolve-helper';
215
+ const helperRes = await fetch(helperEndpoint, {
216
+ method: 'POST',
217
+ headers: {
218
+ 'content-type': 'application/json',
219
+ authorization: 'Bearer ' + RELAY_SECRET,
220
+ },
221
+ body: JSON.stringify({
222
+ videoId,
223
+ client: cl,
224
+ cookie: cl.cookie || '',
225
+ }),
226
+ signal: ac.signal,
227
+ });
228
+ clearTimeout(t);
229
+ if (helperRes.ok) {
230
+ const j = await helperRes.json();
231
+ if (j && j.source) {
232
+ j.source = j.source + '+cloud';
233
+ return j;
234
+ }
235
+ }
236
+ } catch (_) {
237
+ clearTimeout(t);
238
+ }
239
+ }
240
+
241
+ const endpoint =
242
+ (cl.host || 'https://www.youtube.com') +
243
+ '/youtubei/v1/player?prettyPrint=false&key=' +
244
+ encodeURIComponent(cl.apiKey);
245
+
246
+ const client = {
247
+ clientName: cl.clientName,
248
+ clientVersion: cl.clientVersion,
249
+ hl: cl.hl || 'en',
250
+ gl: cl.gl || 'US',
251
+ userAgent: cl.ua,
252
+ ...(cl.extra || {}),
253
+ };
254
+
255
+ const body = {
256
+ context: { client, user: { lockedSafetyMode: false } },
257
+ videoId,
258
+ contentCheckOk: true,
259
+ racyCheckOk: true,
260
+ };
261
+
262
+ if (cl.playbackContext) {
263
+ body.playbackContext = cl.playbackContext;
264
+ }
265
+
266
+ const isWebFamily = /WEB|TVHTML5|MWEB/i.test(cl.clientName);
267
+ const headers = {
268
+ 'content-type': 'application/json',
269
+ 'user-agent': cl.ua,
270
+ 'x-youtube-client-name': String(cl.clientNameHeader || ''),
271
+ 'x-youtube-client-version': cl.clientVersion,
272
+ origin: 'https://www.youtube.com',
273
+ referer: cl.embedded
274
+ ? 'https://www.youtube.com/embed/' + videoId
275
+ : 'https://www.youtube.com/watch?v=' + videoId,
276
+ 'accept-language': 'he-IL,he;q=0.9,en-US;q=0.8,en;q=0.7',
277
+ 'x-goog-api-key': cl.apiKey,
278
+ };
279
+ if (cl.cookie) {
280
+ headers.cookie = cl.cookie;
281
+ // WEB-family: SAPISIDHASH proves cookie ownership (fixes many LOGIN_REQUIRED / bot checks)
282
+ if (isWebFamily || cl.forceAuth) {
283
+ const auth = computeSapisidHash(cl.cookie);
284
+ if (auth) {
285
+ headers.authorization = auth;
286
+ headers['x-origin'] = 'https://www.youtube.com';
287
+ headers['x-goog-authuser'] = '0';
288
+ }
289
+ }
290
+ }
291
+
292
+ const ac = new AbortController();
293
+ const t = setTimeout(() => ac.abort(), cl.timeoutMs || 12000);
294
+ try {
295
+ const r = await fetch(endpoint, {
296
+ method: 'POST',
297
+ headers,
298
+ body: JSON.stringify(body),
299
+ signal: ac.signal,
300
+ });
301
+ const j = await r.json().catch(() => null);
302
+ const formats = collectFormats(j);
303
+ const playability = (j && j.playabilityStatus) || {};
304
+ const status = playability.status || null;
305
+ const reason = playability.reason || playability.messages?.[0] || null;
306
+ const errScreen = playability.errorScreen || null;
307
+ let errorCode = null;
308
+ try {
309
+ errorCode =
310
+ errScreen?.playerErrorMessageRenderer?.errorMessage?.runs?.[0]?.text ||
311
+ errScreen?.playerErrorMessageRenderer?.reason?.simpleText ||
312
+ playability.reason ||
313
+ null;
314
+ } catch (_) {}
315
+ return {
316
+ ok: formats.length > 0 && status !== 'LOGIN_REQUIRED' && status !== 'UNPLAYABLE',
317
+ source: cl.clientName + (cl.cookie ? '+ck' : ''),
318
+ http: r.status,
319
+ playability: {
320
+ status,
321
+ reason,
322
+ errorCode: errorCode || null,
323
+ },
324
+ formats,
325
+ rawStatus: status,
326
+ };
327
+ } catch (e) {
328
+ return {
329
+ ok: false,
330
+ source: cl.clientName + (cl.cookie ? '+ck' : ''),
331
+ error: String(e && e.message ? e.message : e),
332
+ formats: [],
333
+ };
334
+ } finally {
335
+ clearTimeout(t);
336
+ }
337
+ }
338
+
339
+ async function resolvePlayer(videoId, cookieHeader, workerUrl = null) {
340
+ // The freshly auto-loaded Firefox session is the whole point of this relay, so it
341
+ // must win per-cookie over whatever the Worker forwarded — the Worker may send a
342
+ // stale/hardcoded builtin cookie, and slimCookieHeader keeps the LAST value per name,
343
+ // so Firefox goes last. Falls back to the forwarded cookie for any name Firefox lacks.
344
+ const merged = [cookieHeader || '', firefoxCookiesStr || ''].filter(Boolean).join('; ');
345
+ const ck = slimCookieHeader(merged);
346
+ const hasAuth = /SAPISID|__Secure-3PAPISID|LOGIN_INFO|__Secure-3PSID/i.test(ck);
347
+
348
+ // Order matters: authenticated WEB first for Restricted Mode / age / bot checks.
349
+ // ANDROID without cookies often hits MRM / LOGIN_REQUIRED on filtered ISPs.
350
+ const clients = [];
351
+
352
+ if (hasAuth) {
353
+ clients.push({
354
+ clientName: 'WEB',
355
+ clientVersion: '2.20250620.00.00',
356
+ clientNameHeader: '1',
357
+ ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
358
+ apiKey: INNERTUBE_KEY_WEB,
359
+ host: 'https://www.youtube.com',
360
+ cookie: ck,
361
+ forceAuth: true,
362
+ playbackContext: {
363
+ contentPlaybackContext: {
364
+ html5Preference: 'HTML5_PREF_WANTS',
365
+ signatureTimestamp: 20468,
366
+ },
367
+ },
368
+ timeoutMs: 14000,
369
+ });
370
+ clients.push({
371
+ clientName: 'MWEB',
372
+ clientVersion: '2.20250620.00.00',
373
+ clientNameHeader: '2',
374
+ ua: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36',
375
+ apiKey: INNERTUBE_KEY_WEB,
376
+ host: 'https://www.youtube.com',
377
+ cookie: ck,
378
+ forceAuth: true,
379
+ playbackContext: {
380
+ contentPlaybackContext: {
381
+ html5Preference: 'HTML5_PREF_WANTS',
382
+ signatureTimestamp: 20468,
383
+ },
384
+ },
385
+ timeoutMs: 12000,
386
+ });
387
+ // Mobile clients WITH account cookies (helps age-gate; not always)
388
+ clients.push({
389
+ clientName: 'ANDROID',
390
+ clientVersion: '20.50.37',
391
+ clientNameHeader: '3',
392
+ ua: 'com.google.android.youtube/20.50.37 (Linux; U; Android 14) gzip',
393
+ apiKey: INNERTUBE_KEY_ANDROID,
394
+ host: 'https://youtubei.googleapis.com',
395
+ cookie: ck,
396
+ forceAuth: true,
397
+ extra: { androidSdkVersion: 34, osName: 'Android', osVersion: '14' },
398
+ timeoutMs: 12000,
399
+ });
400
+ }
401
+
402
+ // Embedded TV player — sometimes returns progressive formats without login
403
+ clients.push({
404
+ clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
405
+ clientVersion: '2.0',
406
+ clientNameHeader: '85',
407
+ ua: 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
408
+ apiKey: INNERTUBE_KEY_WEB,
409
+ host: 'https://www.youtube.com',
410
+ embedded: true,
411
+ cookie: hasAuth ? ck : undefined,
412
+ playbackContext: {
413
+ contentPlaybackContext: {
414
+ html5Preference: 'HTML5_PREF_WANTS',
415
+ signatureTimestamp: 20468,
416
+ },
417
+ },
418
+ timeoutMs: 12000,
419
+ });
420
+
421
+ // Anonymous ANDROID / IOS fallback (works for many non-restricted videos)
422
+ clients.push(
423
+ {
424
+ clientName: 'ANDROID',
425
+ clientVersion: '20.50.37',
426
+ clientNameHeader: '3',
427
+ ua: 'com.google.android.youtube/20.50.37 (Linux; U; Android 14) gzip',
428
+ apiKey: INNERTUBE_KEY_ANDROID,
429
+ host: 'https://youtubei.googleapis.com',
430
+ extra: { androidSdkVersion: 34, osName: 'Android', osVersion: '14' },
431
+ timeoutMs: 12000,
432
+ },
433
+ {
434
+ clientName: 'IOS',
435
+ clientVersion: '20.12.4',
436
+ clientNameHeader: '5',
437
+ ua: 'com.google.ios.youtube/20.12.4 (iPhone16,2; U; CPU iOS 18_0 like Mac OS X;)',
438
+ apiKey: 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc',
439
+ host: 'https://www.youtube.com',
440
+ extra: {
441
+ deviceModel: 'iPhone16,2',
442
+ osName: 'iPhone',
443
+ osVersion: '18.0',
444
+ },
445
+ timeoutMs: 12000,
446
+ }
447
+ );
448
+
449
+ if (!hasAuth) {
450
+ clients.push({
451
+ clientName: 'WEB',
452
+ clientVersion: '2.20250620.00.00',
453
+ clientNameHeader: '1',
454
+ ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
455
+ apiKey: INNERTUBE_KEY_WEB,
456
+ host: 'https://www.youtube.com',
457
+ playbackContext: {
458
+ contentPlaybackContext: {
459
+ html5Preference: 'HTML5_PREF_WANTS',
460
+ signatureTimestamp: 20468,
461
+ },
462
+ },
463
+ timeoutMs: 12000,
464
+ });
465
+ }
466
+
467
+ const attempts = [];
468
+ // Sequential for auth clients first would be slower; race all, pick best
469
+ const results = await Promise.all(clients.map((c) => tryPlayerClient(videoId, c, workerUrl)));
470
+
471
+ // Prefer any ok with formats; among failures surface LOGIN_REQUIRED / MRM clearly
472
+ let bestFail = null;
473
+ for (const r of results) {
474
+ attempts.push({
475
+ source: r.source,
476
+ ok: r.ok,
477
+ http: r.http,
478
+ status: r.playability?.status,
479
+ reason: r.playability?.reason || r.playability?.errorCode,
480
+ n: (r.formats || []).length,
481
+ error: r.error,
482
+ });
483
+ if (r.ok && r.formats.length) {
484
+ return {
485
+ ok: true,
486
+ videoId,
487
+ client: r.source,
488
+ playability: r.playability,
489
+ formats: r.formats,
490
+ attempts,
491
+ hasAuthCookies: hasAuth,
492
+ egress: 'home-relay',
493
+ };
494
+ }
495
+ if (
496
+ r.playability?.status === 'LOGIN_REQUIRED' ||
497
+ /MRM|bot|restricted|login/i.test(String(r.playability?.reason || r.playability?.errorCode || ''))
498
+ ) {
499
+ bestFail = r;
500
+ }
501
+ }
502
+
503
+ const last = bestFail || results[results.length - 1] || {};
504
+ return {
505
+ ok: false,
506
+ videoId,
507
+ playability: last.playability || { status: 'ERROR' },
508
+ formats: [],
509
+ attempts,
510
+ hasAuthCookies: hasAuth,
511
+ hint: hasAuth
512
+ ? 'Auth cookies present but YouTube still blocked — try re-login in Firefox, disable Restricted Mode on Google account, or use mobile hotspot without Etrog for test.'
513
+ : 'No SAPISID/LOGIN_INFO from Firefox — open YouTube in Firefox while logged in, then restart relay.',
514
+ egress: 'home-relay',
515
+ };
516
+ }
517
+
518
+ async function proxyFetch(urlStr, opts = {}) {
519
+ const u = assertAllowedUrl(urlStr);
520
+ const method = (opts.method || 'GET').toUpperCase();
521
+ const headers = { ...(opts.headers || {}) };
522
+ // Strip hop-by-hop / dangerous
523
+ for (const k of [
524
+ 'host',
525
+ 'connection',
526
+ 'content-length',
527
+ 'transfer-encoding',
528
+ 'authorization', // never forward Worker→relay auth upstream
529
+ ]) {
530
+ delete headers[k];
531
+ delete headers[k.toLowerCase()];
532
+ }
533
+
534
+ const ac = new AbortController();
535
+ const t = setTimeout(() => ac.abort(), opts.timeoutMs || FETCH_TIMEOUT_MS);
536
+ try {
537
+ const init = {
538
+ method,
539
+ headers,
540
+ redirect: 'follow',
541
+ signal: ac.signal,
542
+ };
543
+ if (opts.body != null && method !== 'GET' && method !== 'HEAD') {
544
+ init.body = opts.body;
545
+ }
546
+ const upstream = await fetch(u.toString(), init);
547
+ return upstream;
548
+ } finally {
549
+ clearTimeout(t);
550
+ }
551
+ }
552
+
553
+ function pipeWebToNode(webRes, nodeRes, extraHeaders = {}) {
554
+ const headers = { ...extraHeaders };
555
+ // Forward useful upstream headers
556
+ const pass = [
557
+ 'content-type',
558
+ 'content-length',
559
+ 'content-range',
560
+ 'accept-ranges',
561
+ 'cache-control',
562
+ 'etag',
563
+ 'last-modified',
564
+ ];
565
+ for (const name of pass) {
566
+ const v = webRes.headers.get(name);
567
+ if (v) headers[name] = v;
568
+ }
569
+ headers['x-relay'] = 'home';
570
+ headers['access-control-allow-origin'] = '*';
571
+ headers['access-control-expose-headers'] =
572
+ 'Content-Length, Content-Range, Accept-Ranges, Content-Type, X-Relay';
573
+
574
+ nodeRes.writeHead(webRes.status, headers);
575
+
576
+ if (!webRes.body) {
577
+ nodeRes.end();
578
+ return Promise.resolve();
579
+ }
580
+
581
+ const readable = Readable.fromWeb(webRes.body);
582
+ readable.pipe(nodeRes);
583
+
584
+ return new Promise((resolve) => {
585
+ readable.on('end', resolve);
586
+ readable.on('error', (err) => {
587
+ log('pipe error', err.message);
588
+ try { nodeRes.destroy(err); } catch (_) {}
589
+ resolve();
590
+ });
591
+ });
592
+ }
593
+
594
+ // ── server ───────────────────────────────────────────────────────────────
595
+
596
+ const server = http.createServer(async (req, res) => {
597
+ const reqId = randomUUID().slice(0, 8);
598
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
599
+
600
+ // CORS preflight (Worker browser tooling / curl)
601
+ if (req.method === 'OPTIONS') {
602
+ res.writeHead(204, {
603
+ 'access-control-allow-origin': '*',
604
+ 'access-control-allow-methods': 'GET,POST,OPTIONS',
605
+ 'access-control-allow-headers': 'Authorization, Content-Type, X-Relay-Secret, Range',
606
+ 'access-control-max-age': '86400',
607
+ });
608
+ res.end();
609
+ return;
610
+ }
611
+
612
+ // Health must stay synchronous and tiny — used by start scripts + tunnel probes
613
+ if (url.pathname === '/health' || url.pathname === '/') {
614
+ json(res, 200, {
615
+ ok: true,
616
+ service: 'etrog-yt-home-relay',
617
+ secretConfigured: !!RELAY_SECRET,
618
+ host: HOST,
619
+ port: PORT,
620
+ cookies: firefoxCookiesStr ? 'loaded' : 'none',
621
+ time: new Date().toISOString(),
622
+ });
623
+ return;
624
+ }
625
+
626
+ if (!checkAuth(req)) {
627
+ log(reqId, '401', req.method, url.pathname);
628
+ unauthorized(res);
629
+ return;
630
+ }
631
+
632
+ try {
633
+ // ── POST /v1/yt/player ──
634
+ if (req.method === 'POST' && url.pathname === '/v1/yt/player') {
635
+ const raw = await readBody(req);
636
+ let body = {};
637
+ try {
638
+ body = JSON.parse(raw.toString('utf8') || '{}');
639
+ } catch {
640
+ return badRequest(res, 'invalid json');
641
+ }
642
+ const videoId = String(body.videoId || body.v || '').trim();
643
+ if (!/^[a-zA-Z0-9_-]{6,20}$/.test(videoId)) {
644
+ return badRequest(res, 'invalid videoId');
645
+ }
646
+ log(reqId, 'player', videoId);
647
+ const workerUrl = req.headers['x-worker-url'] || null;
648
+ const pack = await resolvePlayer(videoId, body.cookie || firefoxCookiesStr || '', workerUrl);
649
+ log(
650
+ reqId,
651
+ 'player done',
652
+ videoId,
653
+ pack.ok ? `ok n=${pack.formats.length} via ${pack.client}` : 'fail',
654
+ pack.playability?.status || ''
655
+ );
656
+ return json(res, pack.ok ? 200 : 502, pack);
657
+ }
658
+
659
+ // ── GET /v1/fetch?url= ──
660
+ if (req.method === 'GET' && url.pathname === '/v1/fetch') {
661
+ const target = url.searchParams.get('url');
662
+ if (!target) return badRequest(res, 'missing url');
663
+ const range = req.headers.range;
664
+ const headers = {
665
+ 'user-agent':
666
+ req.headers['x-upstream-ua'] ||
667
+ 'com.google.android.youtube/20.50.37 (Linux; U; Android 14) gzip',
668
+ accept: '*/*',
669
+ origin: 'https://www.youtube.com',
670
+ referer: 'https://www.youtube.com/',
671
+ };
672
+ if (range) headers.range = range;
673
+ log(reqId, 'fetch GET', target.slice(0, 80));
674
+ try {
675
+ const upstream = await proxyFetch(target, { method: 'GET', headers });
676
+ log(reqId, 'upstream status:', upstream.status);
677
+ await pipeWebToNode(upstream, res);
678
+ log(reqId, 'pipe complete');
679
+ } catch (err) {
680
+ log(reqId, 'fetch error:', err.message || err);
681
+ throw err;
682
+ }
683
+ return;
684
+ }
685
+
686
+ // ── POST /v1/fetch ──
687
+ if (req.method === 'POST' && url.pathname === '/v1/fetch') {
688
+ const raw = await readBody(req, 8 * 1024 * 1024);
689
+ let body = {};
690
+ try {
691
+ body = JSON.parse(raw.toString('utf8') || '{}');
692
+ } catch {
693
+ return badRequest(res, 'invalid json');
694
+ }
695
+ if (!body.url) return badRequest(res, 'missing url');
696
+ const method = (body.method || 'GET').toUpperCase();
697
+ const headers = body.headers || {};
698
+ let upstreamBody = body.body;
699
+ if (body.bodyBase64) {
700
+ upstreamBody = Buffer.from(body.bodyBase64, 'base64');
701
+ }
702
+ // Response mode: stream (default for media) or json envelope
703
+ const mode = body.mode || 'stream';
704
+ log(reqId, 'fetch POST', method, String(body.url).slice(0, 80));
705
+ const upstream = await proxyFetch(body.url, {
706
+ method,
707
+ headers,
708
+ body: upstreamBody,
709
+ });
710
+
711
+ if (mode === 'json') {
712
+ const buf = Buffer.from(await upstream.arrayBuffer());
713
+ const ct = upstream.headers.get('content-type') || '';
714
+ let parsed = null;
715
+ if (ct.includes('json') || buf[0] === 0x7b) {
716
+ try {
717
+ parsed = JSON.parse(buf.toString('utf8'));
718
+ } catch {
719
+ /* keep null */
720
+ }
721
+ }
722
+ return json(res, 200, {
723
+ ok: upstream.ok,
724
+ status: upstream.status,
725
+ headers: Object.fromEntries(upstream.headers.entries()),
726
+ bodyBase64: parsed ? undefined : buf.toString('base64'),
727
+ json: parsed,
728
+ });
729
+ }
730
+ return pipeWebToNode(upstream, res);
731
+ }
732
+
733
+ json(res, 404, { error: 'not found', path: url.pathname });
734
+ } catch (e) {
735
+ log(reqId, 'error', e.message || e);
736
+ json(res, 502, { error: String(e && e.message ? e.message : e) });
737
+ }
738
+ });
739
+
740
+ if (!RELAY_SECRET) {
741
+ console.error('');
742
+ console.error('ERROR: set RELAY_SECRET to a long random string before starting.');
743
+ console.error(' Windows: set RELAY_SECRET=your-secret-here');
744
+ console.error(' PowerShell: $env:RELAY_SECRET="your-secret-here"');
745
+ console.error(' Linux/mac: export RELAY_SECRET=your-secret-here');
746
+ console.error('');
747
+ process.exit(1);
748
+ }
749
+
750
+ let firefoxCookiesStr = '';
751
+
752
+ function extractFirefoxCookies() {
753
+ return new Promise((resolve) => {
754
+ const appData = process.env.APPDATA;
755
+ if (!appData) {
756
+ log('[Firefox Cookies] APPDATA env var not found.');
757
+ return resolve();
758
+ }
759
+
760
+ const profilesDir = path.join(appData, 'Mozilla', 'Firefox', 'Profiles');
761
+ if (!fs.existsSync(profilesDir)) {
762
+ log('[Firefox Cookies] Firefox profiles directory not found.');
763
+ return resolve();
764
+ }
765
+
766
+ try {
767
+ const profiles = fs.readdirSync(profilesDir);
768
+ let cookiesPath = null;
769
+
770
+ for (const profile of profiles) {
771
+ const p = path.join(profilesDir, profile, 'cookies.sqlite');
772
+ if (fs.existsSync(p)) {
773
+ cookiesPath = p;
774
+ if (profile.includes('default-release')) break;
775
+ }
776
+ }
777
+
778
+ if (!cookiesPath) {
779
+ log('[Firefox Cookies] cookies.sqlite not found.');
780
+ return resolve();
781
+ }
782
+
783
+ log(`[Firefox Cookies] Reading database from: ${cookiesPath}`);
784
+ const tempDbPath = path.join(process.env.TEMP || '.', `cookies_temp_${Date.now()}.sqlite`);
785
+ fs.copyFileSync(cookiesPath, tempDbPath);
786
+
787
+ const db = new sqlite3.Database(tempDbPath, (openErr) => {
788
+ if (openErr) {
789
+ log(`[Firefox Cookies] Open error: ${openErr.message}`);
790
+ try { fs.unlinkSync(tempDbPath); } catch {}
791
+ return resolve();
792
+ }
793
+ // youtube.com + google.com (SAPISID/SID often live on .google.com)
794
+ db.all(
795
+ `SELECT name, value, host FROM moz_cookies
796
+ WHERE (host LIKE '%youtube.com' OR host LIKE '%google.com')
797
+ AND name IN (
798
+ 'LOGIN_INFO','SID','HSID','SSID','APISID','SAPISID',
799
+ '__Secure-1PSID','__Secure-3PSID','__Secure-1PAPISID','__Secure-3PAPISID',
800
+ '__Secure-1PSIDTS','__Secure-3PSIDTS','__Secure-1PSIDCC','__Secure-3PSIDCC',
801
+ 'PREF','SOCS','VISITOR_INFO1_LIVE','YSC','CONSENT'
802
+ )`,
803
+ [],
804
+ (err, rows) => {
805
+ db.close(() => {
806
+ try { fs.unlinkSync(tempDbPath); } catch {}
807
+ });
808
+ if (err) {
809
+ log(`[Firefox Cookies] Query error: ${err.message}`);
810
+ return resolve();
811
+ }
812
+ if (rows && rows.length > 0) {
813
+ // Prefer .youtube.com over .google.com when same name
814
+ const byName = new Map();
815
+ const score = (host) =>
816
+ String(host).includes('youtube.com') ? 2 : String(host).includes('google.com') ? 1 : 0;
817
+ for (const r of rows) {
818
+ const prev = byName.get(r.name);
819
+ if (!prev || score(r.host) >= score(prev.host)) byName.set(r.name, r);
820
+ }
821
+ firefoxCookiesStr = [...byName.values()].map((r) => `${r.name}=${r.value}`).join('; ');
822
+ const hasSapisid = byName.has('SAPISID') || byName.has('__Secure-3PAPISID');
823
+ const hasLogin = byName.has('LOGIN_INFO') || byName.has('__Secure-3PSID');
824
+ log(
825
+ `[Firefox Cookies] Extracted ${byName.size} auth cookies` +
826
+ ` (SAPISID=${hasSapisid ? 'yes' : 'NO'}, session=${hasLogin ? 'yes' : 'NO'}).`
827
+ );
828
+ if (!hasSapisid || !hasLogin) {
829
+ log('[Firefox Cookies] WARNING: incomplete session — log into YouTube in Firefox and restart relay.');
830
+ }
831
+ } else {
832
+ log('[Firefox Cookies] No cookies found for youtube/google.');
833
+ }
834
+ resolve();
835
+ }
836
+ );
837
+ });
838
+ } catch (e) {
839
+ log(`[Firefox Cookies] Error: ${e.message}`);
840
+ resolve();
841
+ }
842
+ });
843
+ }
844
+
845
+ server.on('error', (err) => {
846
+ if (err && err.code === 'EADDRINUSE') {
847
+ console.error(`Port ${PORT} already in use. Run stop-relay.ps1 or: Get-NetTCPConnection -LocalPort ${PORT}`);
848
+ } else {
849
+ console.error('Server listen error:', err);
850
+ }
851
+ process.exit(1);
852
+ });
853
+
854
+ // Listen FIRST so /health works immediately; cookies load in background.
855
+ // Tunnel is external (start-relay.ps1 → cloudflared) — not embedded localtunnel.
856
+ // Why: localtunnel often returns a *different* URL than the requested subdomain,
857
+ // while start scripts hardcoded etrog-bypass-mirik-999.loca.lt → Worker 502.
858
+ server.listen(PORT, HOST, () => {
859
+ log(`YT home relay listening on http://${HOST}:${PORT}`);
860
+ log('Health: GET http://127.0.0.1:' + PORT + '/health');
861
+ log('Expose with cloudflared (preferred):');
862
+ log(' cloudflared tunnel --url http://127.0.0.1:' + PORT);
863
+ log('Or run ..\\start-relay.ps1 from the project root.');
864
+
865
+ // Non-blocking cookie load (Firefox may lock cookies.sqlite)
866
+ extractFirefoxCookies().catch((e) => log('[Firefox Cookies]', e.message || e));
867
+ // Refresh often — YouTube rotates PSIDTS; 24h was too stale for bot checks
868
+ setInterval(() => {
869
+ extractFirefoxCookies().catch(() => {});
870
+ }, 15 * 60 * 1000);
871
+ });
install-autostart.ps1 ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # install-autostart.ps1
2
+ # Registers a Windows Scheduled Task that starts the home relay + cloudflared
3
+ # at user logon — more reliable than Startup-folder VBS (Hebrew paths, no window).
4
+ #
5
+ # Run once (as the same user who uses Firefox/YouTube):
6
+ # powershell -ExecutionPolicy Bypass -File .\install-autostart.ps1
7
+ #
8
+ # Prerequisites:
9
+ # - CLOUDFLARE_API_TOKEN already set as User environment variable
10
+ # - cloudflared installed
11
+ # - start-relay.ps1 works when run manually
12
+
13
+ $ErrorActionPreference = "Stop"
14
+ $TaskName = "EtrogYtHomeRelay"
15
+ $ProjectDir = $PSScriptRoot
16
+ $StartScript = Join-Path $ProjectDir "start-relay.ps1"
17
+
18
+ if (-not (Test-Path $StartScript)) {
19
+ Write-Host "ERROR: missing $StartScript" -ForegroundColor Red
20
+ exit 1
21
+ }
22
+
23
+ # Verify token is available to this user (User-level env)
24
+ $token = [Environment]::GetEnvironmentVariable("CLOUDFLARE_API_TOKEN", "User")
25
+ if (-not $token) {
26
+ $token = $env:CLOUDFLARE_API_TOKEN
27
+ }
28
+ if (-not $token) {
29
+ Write-Host "WARNING: CLOUDFLARE_API_TOKEN not found in User env." -ForegroundColor Yellow
30
+ Write-Host " Set it once:" -ForegroundColor Yellow
31
+ Write-Host ' [Environment]::SetEnvironmentVariable("CLOUDFLARE_API_TOKEN","YOUR_TOKEN","User")' -ForegroundColor Gray
32
+ Write-Host " Then log off/on (or reboot) so Scheduled Tasks inherit it." -ForegroundColor Gray
33
+ }
34
+
35
+ # Optional: RELAY_INSECURE_TLS for filter MITM cert errors
36
+ # [Environment]::SetEnvironmentVariable("RELAY_INSECURE_TLS","1","User")
37
+
38
+ $psExe = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
39
+ $arg = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$StartScript`""
40
+
41
+ # Remove old task if present
42
+ try { schtasks /Delete /TN $TaskName /F 2>$null } catch {}
43
+
44
+ # Create: at logon, highest privileges not required, run whether user is logged on only when logged on
45
+ # Delay 45s so network + Firefox profile are ready
46
+ $xml = @"
47
+ <?xml version="1.0" encoding="UTF-16"?>
48
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
49
+ <RegistrationInfo>
50
+ <Description>Etrog YT Home Relay + cloudflared (auto start)</Description>
51
+ </RegistrationInfo>
52
+ <Triggers>
53
+ <LogonTrigger>
54
+ <Enabled>true</Enabled>
55
+ <Delay>PT45S</Delay>
56
+ </LogonTrigger>
57
+ </Triggers>
58
+ <Principals>
59
+ <Principal id="Author">
60
+ <LogonType>InteractiveToken</LogonType>
61
+ <RunLevel>LeastPrivilege</RunLevel>
62
+ </Principal>
63
+ </Principals>
64
+ <Settings>
65
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
66
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
67
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
68
+ <AllowHardTerminate>true</AllowHardTerminate>
69
+ <StartWhenAvailable>true</StartWhenAvailable>
70
+ <RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
71
+ <IdleSettings>
72
+ <StopOnIdleEnd>false</StopOnIdleEnd>
73
+ <RestartOnIdle>false</RestartOnIdle>
74
+ </IdleSettings>
75
+ <AllowStartOnDemand>true</AllowStartOnDemand>
76
+ <Enabled>true</Enabled>
77
+ <Hidden>true</Hidden>
78
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
79
+ <Priority>7</Priority>
80
+ <RestartOnFailure>
81
+ <Interval>PT2M</Interval>
82
+ <Count>5</Count>
83
+ </RestartOnFailure>
84
+ </Settings>
85
+ <Actions Context="Author">
86
+ <Exec>
87
+ <Command>$psExe</Command>
88
+ <Arguments>$arg</Arguments>
89
+ <WorkingDirectory>$ProjectDir</WorkingDirectory>
90
+ </Exec>
91
+ </Actions>
92
+ </Task>
93
+ "@
94
+
95
+ $xmlPath = Join-Path $env:TEMP "etrog-relay-task.xml"
96
+ # Task Scheduler wants UTF-16 LE XML
97
+ [System.IO.File]::WriteAllText($xmlPath, $xml, [System.Text.Encoding]::Unicode)
98
+
99
+ schtasks /Create /TN $TaskName /XML $xmlPath /F | Out-Host
100
+ Remove-Item $xmlPath -ErrorAction SilentlyContinue
101
+
102
+ # Also ensure Startup VBS (if any) does not double-start — optional note
103
+ $startup = [Environment]::GetFolderPath("Startup")
104
+ $vbs = Join-Path $startup "start_bypass_relay.vbs"
105
+ if (Test-Path $vbs) {
106
+ Write-Host "NOTE: Found Startup VBS: $vbs" -ForegroundColor Yellow
107
+ Write-Host " Prefer ONLY the Scheduled Task (remove VBS to avoid double start)." -ForegroundColor Yellow
108
+ Write-Host " Remove-Item `"$vbs`"" -ForegroundColor Gray
109
+ }
110
+
111
+ Write-Host ""
112
+ Write-Host "Installed task: $TaskName" -ForegroundColor Green
113
+ Write-Host " Test now: schtasks /Run /TN $TaskName" -ForegroundColor Cyan
114
+ Write-Host " Status: schtasks /Query /TN $TaskName /V /FO LIST" -ForegroundColor Cyan
115
+ Write-Host " Remove: schtasks /Delete /TN $TaskName /F" -ForegroundColor Cyan
116
+ Write-Host ""
117
+ Write-Host "After reboot: wait ~1 min, then:" -ForegroundColor Cyan
118
+ Write-Host " Invoke-RestMethod http://127.0.0.1:8788/health" -ForegroundColor Gray
119
+ Write-Host " Get-Content `$env:TEMP\yt-relay-url.txt" -ForegroundColor Gray
package-lock.json ADDED
@@ -0,0 +1,1780 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "etrog-bypass-proxy",
3
+ "version": "2.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "etrog-bypass-proxy",
9
+ "version": "2.0.0",
10
+ "devDependencies": {
11
+ "localtunnel": "^2.0.2",
12
+ "wrangler": "^4.110.0"
13
+ }
14
+ },
15
+ "node_modules/@cloudflare/kv-asset-handler": {
16
+ "version": "0.5.0",
17
+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
18
+ "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",
19
+ "dev": true,
20
+ "license": "MIT OR Apache-2.0",
21
+ "engines": {
22
+ "node": ">=22.0.0"
23
+ }
24
+ },
25
+ "node_modules/@cloudflare/unenv-preset": {
26
+ "version": "2.16.1",
27
+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",
28
+ "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",
29
+ "dev": true,
30
+ "license": "MIT OR Apache-2.0",
31
+ "peerDependencies": {
32
+ "unenv": "2.0.0-rc.24",
33
+ "workerd": ">1.20260305.0 <2.0.0-0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "workerd": {
37
+ "optional": true
38
+ }
39
+ }
40
+ },
41
+ "node_modules/@cloudflare/workerd-darwin-64": {
42
+ "version": "1.20260710.1",
43
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260710.1.tgz",
44
+ "integrity": "sha512-OqJl2eWF5+y9jarMm3YqqCTUe7Hd4ihogX5jyRU8iaAgOVyDr/Bk6aXpPCVUi1/MHzO93a18R/TmSTtzmB0sQw==",
45
+ "cpu": [
46
+ "x64"
47
+ ],
48
+ "dev": true,
49
+ "license": "Apache-2.0",
50
+ "optional": true,
51
+ "os": [
52
+ "darwin"
53
+ ],
54
+ "engines": {
55
+ "node": ">=16"
56
+ }
57
+ },
58
+ "node_modules/@cloudflare/workerd-darwin-arm64": {
59
+ "version": "1.20260710.1",
60
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260710.1.tgz",
61
+ "integrity": "sha512-MYBqWgUblO+VlGvO73zYsH3hB9tdRj+yLyt5IHDFWryipb2l1efmNiWtAOkIhSRfypqLYGFrfpaDm2Hg00XVKw==",
62
+ "cpu": [
63
+ "arm64"
64
+ ],
65
+ "dev": true,
66
+ "license": "Apache-2.0",
67
+ "optional": true,
68
+ "os": [
69
+ "darwin"
70
+ ],
71
+ "engines": {
72
+ "node": ">=16"
73
+ }
74
+ },
75
+ "node_modules/@cloudflare/workerd-linux-64": {
76
+ "version": "1.20260710.1",
77
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260710.1.tgz",
78
+ "integrity": "sha512-lVWUgqI8qrkqvaCBGElu1kdaUFdAvaS2RD8K4qkCFP9hI3f5TCXumEs5qWSeZkvKum0+X/uJZ5hBFWsYI5SmoQ==",
79
+ "cpu": [
80
+ "x64"
81
+ ],
82
+ "dev": true,
83
+ "license": "Apache-2.0",
84
+ "optional": true,
85
+ "os": [
86
+ "linux"
87
+ ],
88
+ "engines": {
89
+ "node": ">=16"
90
+ }
91
+ },
92
+ "node_modules/@cloudflare/workerd-linux-arm64": {
93
+ "version": "1.20260710.1",
94
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260710.1.tgz",
95
+ "integrity": "sha512-kDwDPItBjAI4JL0df9Fma2N+Qggbm77IB/DnroAkEGQ79fpR80sYMyuB/ZQKyjEk9f48Ocq7HCCLq59qVSyNqA==",
96
+ "cpu": [
97
+ "arm64"
98
+ ],
99
+ "dev": true,
100
+ "license": "Apache-2.0",
101
+ "optional": true,
102
+ "os": [
103
+ "linux"
104
+ ],
105
+ "engines": {
106
+ "node": ">=16"
107
+ }
108
+ },
109
+ "node_modules/@cloudflare/workerd-windows-64": {
110
+ "version": "1.20260710.1",
111
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260710.1.tgz",
112
+ "integrity": "sha512-GcLHy1oN1dfK6g1Z7UDV9f5xMGyTfPwcjWQ0sfWKH31IsoEVCRapnj3IC0PoIrDbnoo6irGPP0CwVs3WzdTajw==",
113
+ "cpu": [
114
+ "x64"
115
+ ],
116
+ "dev": true,
117
+ "license": "Apache-2.0",
118
+ "optional": true,
119
+ "os": [
120
+ "win32"
121
+ ],
122
+ "engines": {
123
+ "node": ">=16"
124
+ }
125
+ },
126
+ "node_modules/@cspotcode/source-map-support": {
127
+ "version": "0.8.1",
128
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
129
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
130
+ "dev": true,
131
+ "license": "MIT",
132
+ "dependencies": {
133
+ "@jridgewell/trace-mapping": "0.3.9"
134
+ },
135
+ "engines": {
136
+ "node": ">=12"
137
+ }
138
+ },
139
+ "node_modules/@emnapi/runtime": {
140
+ "version": "1.11.2",
141
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
142
+ "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
143
+ "dev": true,
144
+ "license": "MIT",
145
+ "optional": true,
146
+ "dependencies": {
147
+ "tslib": "^2.4.0"
148
+ }
149
+ },
150
+ "node_modules/@esbuild/aix-ppc64": {
151
+ "version": "0.28.1",
152
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
153
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
154
+ "cpu": [
155
+ "ppc64"
156
+ ],
157
+ "dev": true,
158
+ "license": "MIT",
159
+ "optional": true,
160
+ "os": [
161
+ "aix"
162
+ ],
163
+ "engines": {
164
+ "node": ">=18"
165
+ }
166
+ },
167
+ "node_modules/@esbuild/android-arm": {
168
+ "version": "0.28.1",
169
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
170
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
171
+ "cpu": [
172
+ "arm"
173
+ ],
174
+ "dev": true,
175
+ "license": "MIT",
176
+ "optional": true,
177
+ "os": [
178
+ "android"
179
+ ],
180
+ "engines": {
181
+ "node": ">=18"
182
+ }
183
+ },
184
+ "node_modules/@esbuild/android-arm64": {
185
+ "version": "0.28.1",
186
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
187
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
188
+ "cpu": [
189
+ "arm64"
190
+ ],
191
+ "dev": true,
192
+ "license": "MIT",
193
+ "optional": true,
194
+ "os": [
195
+ "android"
196
+ ],
197
+ "engines": {
198
+ "node": ">=18"
199
+ }
200
+ },
201
+ "node_modules/@esbuild/android-x64": {
202
+ "version": "0.28.1",
203
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
204
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
205
+ "cpu": [
206
+ "x64"
207
+ ],
208
+ "dev": true,
209
+ "license": "MIT",
210
+ "optional": true,
211
+ "os": [
212
+ "android"
213
+ ],
214
+ "engines": {
215
+ "node": ">=18"
216
+ }
217
+ },
218
+ "node_modules/@esbuild/darwin-arm64": {
219
+ "version": "0.28.1",
220
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
221
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
222
+ "cpu": [
223
+ "arm64"
224
+ ],
225
+ "dev": true,
226
+ "license": "MIT",
227
+ "optional": true,
228
+ "os": [
229
+ "darwin"
230
+ ],
231
+ "engines": {
232
+ "node": ">=18"
233
+ }
234
+ },
235
+ "node_modules/@esbuild/darwin-x64": {
236
+ "version": "0.28.1",
237
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
238
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
239
+ "cpu": [
240
+ "x64"
241
+ ],
242
+ "dev": true,
243
+ "license": "MIT",
244
+ "optional": true,
245
+ "os": [
246
+ "darwin"
247
+ ],
248
+ "engines": {
249
+ "node": ">=18"
250
+ }
251
+ },
252
+ "node_modules/@esbuild/freebsd-arm64": {
253
+ "version": "0.28.1",
254
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
255
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
256
+ "cpu": [
257
+ "arm64"
258
+ ],
259
+ "dev": true,
260
+ "license": "MIT",
261
+ "optional": true,
262
+ "os": [
263
+ "freebsd"
264
+ ],
265
+ "engines": {
266
+ "node": ">=18"
267
+ }
268
+ },
269
+ "node_modules/@esbuild/freebsd-x64": {
270
+ "version": "0.28.1",
271
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
272
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
273
+ "cpu": [
274
+ "x64"
275
+ ],
276
+ "dev": true,
277
+ "license": "MIT",
278
+ "optional": true,
279
+ "os": [
280
+ "freebsd"
281
+ ],
282
+ "engines": {
283
+ "node": ">=18"
284
+ }
285
+ },
286
+ "node_modules/@esbuild/linux-arm": {
287
+ "version": "0.28.1",
288
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
289
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
290
+ "cpu": [
291
+ "arm"
292
+ ],
293
+ "dev": true,
294
+ "license": "MIT",
295
+ "optional": true,
296
+ "os": [
297
+ "linux"
298
+ ],
299
+ "engines": {
300
+ "node": ">=18"
301
+ }
302
+ },
303
+ "node_modules/@esbuild/linux-arm64": {
304
+ "version": "0.28.1",
305
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
306
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
307
+ "cpu": [
308
+ "arm64"
309
+ ],
310
+ "dev": true,
311
+ "license": "MIT",
312
+ "optional": true,
313
+ "os": [
314
+ "linux"
315
+ ],
316
+ "engines": {
317
+ "node": ">=18"
318
+ }
319
+ },
320
+ "node_modules/@esbuild/linux-ia32": {
321
+ "version": "0.28.1",
322
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
323
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
324
+ "cpu": [
325
+ "ia32"
326
+ ],
327
+ "dev": true,
328
+ "license": "MIT",
329
+ "optional": true,
330
+ "os": [
331
+ "linux"
332
+ ],
333
+ "engines": {
334
+ "node": ">=18"
335
+ }
336
+ },
337
+ "node_modules/@esbuild/linux-loong64": {
338
+ "version": "0.28.1",
339
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
340
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
341
+ "cpu": [
342
+ "loong64"
343
+ ],
344
+ "dev": true,
345
+ "license": "MIT",
346
+ "optional": true,
347
+ "os": [
348
+ "linux"
349
+ ],
350
+ "engines": {
351
+ "node": ">=18"
352
+ }
353
+ },
354
+ "node_modules/@esbuild/linux-mips64el": {
355
+ "version": "0.28.1",
356
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
357
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
358
+ "cpu": [
359
+ "mips64el"
360
+ ],
361
+ "dev": true,
362
+ "license": "MIT",
363
+ "optional": true,
364
+ "os": [
365
+ "linux"
366
+ ],
367
+ "engines": {
368
+ "node": ">=18"
369
+ }
370
+ },
371
+ "node_modules/@esbuild/linux-ppc64": {
372
+ "version": "0.28.1",
373
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
374
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
375
+ "cpu": [
376
+ "ppc64"
377
+ ],
378
+ "dev": true,
379
+ "license": "MIT",
380
+ "optional": true,
381
+ "os": [
382
+ "linux"
383
+ ],
384
+ "engines": {
385
+ "node": ">=18"
386
+ }
387
+ },
388
+ "node_modules/@esbuild/linux-riscv64": {
389
+ "version": "0.28.1",
390
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
391
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
392
+ "cpu": [
393
+ "riscv64"
394
+ ],
395
+ "dev": true,
396
+ "license": "MIT",
397
+ "optional": true,
398
+ "os": [
399
+ "linux"
400
+ ],
401
+ "engines": {
402
+ "node": ">=18"
403
+ }
404
+ },
405
+ "node_modules/@esbuild/linux-s390x": {
406
+ "version": "0.28.1",
407
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
408
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
409
+ "cpu": [
410
+ "s390x"
411
+ ],
412
+ "dev": true,
413
+ "license": "MIT",
414
+ "optional": true,
415
+ "os": [
416
+ "linux"
417
+ ],
418
+ "engines": {
419
+ "node": ">=18"
420
+ }
421
+ },
422
+ "node_modules/@esbuild/linux-x64": {
423
+ "version": "0.28.1",
424
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
425
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
426
+ "cpu": [
427
+ "x64"
428
+ ],
429
+ "dev": true,
430
+ "license": "MIT",
431
+ "optional": true,
432
+ "os": [
433
+ "linux"
434
+ ],
435
+ "engines": {
436
+ "node": ">=18"
437
+ }
438
+ },
439
+ "node_modules/@esbuild/netbsd-arm64": {
440
+ "version": "0.28.1",
441
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
442
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
443
+ "cpu": [
444
+ "arm64"
445
+ ],
446
+ "dev": true,
447
+ "license": "MIT",
448
+ "optional": true,
449
+ "os": [
450
+ "netbsd"
451
+ ],
452
+ "engines": {
453
+ "node": ">=18"
454
+ }
455
+ },
456
+ "node_modules/@esbuild/netbsd-x64": {
457
+ "version": "0.28.1",
458
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
459
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
460
+ "cpu": [
461
+ "x64"
462
+ ],
463
+ "dev": true,
464
+ "license": "MIT",
465
+ "optional": true,
466
+ "os": [
467
+ "netbsd"
468
+ ],
469
+ "engines": {
470
+ "node": ">=18"
471
+ }
472
+ },
473
+ "node_modules/@esbuild/openbsd-arm64": {
474
+ "version": "0.28.1",
475
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
476
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
477
+ "cpu": [
478
+ "arm64"
479
+ ],
480
+ "dev": true,
481
+ "license": "MIT",
482
+ "optional": true,
483
+ "os": [
484
+ "openbsd"
485
+ ],
486
+ "engines": {
487
+ "node": ">=18"
488
+ }
489
+ },
490
+ "node_modules/@esbuild/openbsd-x64": {
491
+ "version": "0.28.1",
492
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
493
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
494
+ "cpu": [
495
+ "x64"
496
+ ],
497
+ "dev": true,
498
+ "license": "MIT",
499
+ "optional": true,
500
+ "os": [
501
+ "openbsd"
502
+ ],
503
+ "engines": {
504
+ "node": ">=18"
505
+ }
506
+ },
507
+ "node_modules/@esbuild/openharmony-arm64": {
508
+ "version": "0.28.1",
509
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
510
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
511
+ "cpu": [
512
+ "arm64"
513
+ ],
514
+ "dev": true,
515
+ "license": "MIT",
516
+ "optional": true,
517
+ "os": [
518
+ "openharmony"
519
+ ],
520
+ "engines": {
521
+ "node": ">=18"
522
+ }
523
+ },
524
+ "node_modules/@esbuild/sunos-x64": {
525
+ "version": "0.28.1",
526
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
527
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
528
+ "cpu": [
529
+ "x64"
530
+ ],
531
+ "dev": true,
532
+ "license": "MIT",
533
+ "optional": true,
534
+ "os": [
535
+ "sunos"
536
+ ],
537
+ "engines": {
538
+ "node": ">=18"
539
+ }
540
+ },
541
+ "node_modules/@esbuild/win32-arm64": {
542
+ "version": "0.28.1",
543
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
544
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
545
+ "cpu": [
546
+ "arm64"
547
+ ],
548
+ "dev": true,
549
+ "license": "MIT",
550
+ "optional": true,
551
+ "os": [
552
+ "win32"
553
+ ],
554
+ "engines": {
555
+ "node": ">=18"
556
+ }
557
+ },
558
+ "node_modules/@esbuild/win32-ia32": {
559
+ "version": "0.28.1",
560
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
561
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
562
+ "cpu": [
563
+ "ia32"
564
+ ],
565
+ "dev": true,
566
+ "license": "MIT",
567
+ "optional": true,
568
+ "os": [
569
+ "win32"
570
+ ],
571
+ "engines": {
572
+ "node": ">=18"
573
+ }
574
+ },
575
+ "node_modules/@esbuild/win32-x64": {
576
+ "version": "0.28.1",
577
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
578
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
579
+ "cpu": [
580
+ "x64"
581
+ ],
582
+ "dev": true,
583
+ "license": "MIT",
584
+ "optional": true,
585
+ "os": [
586
+ "win32"
587
+ ],
588
+ "engines": {
589
+ "node": ">=18"
590
+ }
591
+ },
592
+ "node_modules/@img/colour": {
593
+ "version": "1.1.0",
594
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
595
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
596
+ "dev": true,
597
+ "license": "MIT",
598
+ "engines": {
599
+ "node": ">=18"
600
+ }
601
+ },
602
+ "node_modules/@img/sharp-darwin-arm64": {
603
+ "version": "0.34.5",
604
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
605
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
606
+ "cpu": [
607
+ "arm64"
608
+ ],
609
+ "dev": true,
610
+ "license": "Apache-2.0",
611
+ "optional": true,
612
+ "os": [
613
+ "darwin"
614
+ ],
615
+ "engines": {
616
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
617
+ },
618
+ "funding": {
619
+ "url": "https://opencollective.com/libvips"
620
+ },
621
+ "optionalDependencies": {
622
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
623
+ }
624
+ },
625
+ "node_modules/@img/sharp-darwin-x64": {
626
+ "version": "0.34.5",
627
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
628
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
629
+ "cpu": [
630
+ "x64"
631
+ ],
632
+ "dev": true,
633
+ "license": "Apache-2.0",
634
+ "optional": true,
635
+ "os": [
636
+ "darwin"
637
+ ],
638
+ "engines": {
639
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
640
+ },
641
+ "funding": {
642
+ "url": "https://opencollective.com/libvips"
643
+ },
644
+ "optionalDependencies": {
645
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
646
+ }
647
+ },
648
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
649
+ "version": "1.2.4",
650
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
651
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
652
+ "cpu": [
653
+ "arm64"
654
+ ],
655
+ "dev": true,
656
+ "license": "LGPL-3.0-or-later",
657
+ "optional": true,
658
+ "os": [
659
+ "darwin"
660
+ ],
661
+ "funding": {
662
+ "url": "https://opencollective.com/libvips"
663
+ }
664
+ },
665
+ "node_modules/@img/sharp-libvips-darwin-x64": {
666
+ "version": "1.2.4",
667
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
668
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
669
+ "cpu": [
670
+ "x64"
671
+ ],
672
+ "dev": true,
673
+ "license": "LGPL-3.0-or-later",
674
+ "optional": true,
675
+ "os": [
676
+ "darwin"
677
+ ],
678
+ "funding": {
679
+ "url": "https://opencollective.com/libvips"
680
+ }
681
+ },
682
+ "node_modules/@img/sharp-libvips-linux-arm": {
683
+ "version": "1.2.4",
684
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
685
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
686
+ "cpu": [
687
+ "arm"
688
+ ],
689
+ "dev": true,
690
+ "license": "LGPL-3.0-or-later",
691
+ "optional": true,
692
+ "os": [
693
+ "linux"
694
+ ],
695
+ "funding": {
696
+ "url": "https://opencollective.com/libvips"
697
+ }
698
+ },
699
+ "node_modules/@img/sharp-libvips-linux-arm64": {
700
+ "version": "1.2.4",
701
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
702
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
703
+ "cpu": [
704
+ "arm64"
705
+ ],
706
+ "dev": true,
707
+ "license": "LGPL-3.0-or-later",
708
+ "optional": true,
709
+ "os": [
710
+ "linux"
711
+ ],
712
+ "funding": {
713
+ "url": "https://opencollective.com/libvips"
714
+ }
715
+ },
716
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
717
+ "version": "1.2.4",
718
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
719
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
720
+ "cpu": [
721
+ "ppc64"
722
+ ],
723
+ "dev": true,
724
+ "license": "LGPL-3.0-or-later",
725
+ "optional": true,
726
+ "os": [
727
+ "linux"
728
+ ],
729
+ "funding": {
730
+ "url": "https://opencollective.com/libvips"
731
+ }
732
+ },
733
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
734
+ "version": "1.2.4",
735
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
736
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
737
+ "cpu": [
738
+ "riscv64"
739
+ ],
740
+ "dev": true,
741
+ "license": "LGPL-3.0-or-later",
742
+ "optional": true,
743
+ "os": [
744
+ "linux"
745
+ ],
746
+ "funding": {
747
+ "url": "https://opencollective.com/libvips"
748
+ }
749
+ },
750
+ "node_modules/@img/sharp-libvips-linux-s390x": {
751
+ "version": "1.2.4",
752
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
753
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
754
+ "cpu": [
755
+ "s390x"
756
+ ],
757
+ "dev": true,
758
+ "license": "LGPL-3.0-or-later",
759
+ "optional": true,
760
+ "os": [
761
+ "linux"
762
+ ],
763
+ "funding": {
764
+ "url": "https://opencollective.com/libvips"
765
+ }
766
+ },
767
+ "node_modules/@img/sharp-libvips-linux-x64": {
768
+ "version": "1.2.4",
769
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
770
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
771
+ "cpu": [
772
+ "x64"
773
+ ],
774
+ "dev": true,
775
+ "license": "LGPL-3.0-or-later",
776
+ "optional": true,
777
+ "os": [
778
+ "linux"
779
+ ],
780
+ "funding": {
781
+ "url": "https://opencollective.com/libvips"
782
+ }
783
+ },
784
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
785
+ "version": "1.2.4",
786
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
787
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
788
+ "cpu": [
789
+ "arm64"
790
+ ],
791
+ "dev": true,
792
+ "license": "LGPL-3.0-or-later",
793
+ "optional": true,
794
+ "os": [
795
+ "linux"
796
+ ],
797
+ "funding": {
798
+ "url": "https://opencollective.com/libvips"
799
+ }
800
+ },
801
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
802
+ "version": "1.2.4",
803
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
804
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
805
+ "cpu": [
806
+ "x64"
807
+ ],
808
+ "dev": true,
809
+ "license": "LGPL-3.0-or-later",
810
+ "optional": true,
811
+ "os": [
812
+ "linux"
813
+ ],
814
+ "funding": {
815
+ "url": "https://opencollective.com/libvips"
816
+ }
817
+ },
818
+ "node_modules/@img/sharp-linux-arm": {
819
+ "version": "0.34.5",
820
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
821
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
822
+ "cpu": [
823
+ "arm"
824
+ ],
825
+ "dev": true,
826
+ "license": "Apache-2.0",
827
+ "optional": true,
828
+ "os": [
829
+ "linux"
830
+ ],
831
+ "engines": {
832
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
833
+ },
834
+ "funding": {
835
+ "url": "https://opencollective.com/libvips"
836
+ },
837
+ "optionalDependencies": {
838
+ "@img/sharp-libvips-linux-arm": "1.2.4"
839
+ }
840
+ },
841
+ "node_modules/@img/sharp-linux-arm64": {
842
+ "version": "0.34.5",
843
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
844
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
845
+ "cpu": [
846
+ "arm64"
847
+ ],
848
+ "dev": true,
849
+ "license": "Apache-2.0",
850
+ "optional": true,
851
+ "os": [
852
+ "linux"
853
+ ],
854
+ "engines": {
855
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
856
+ },
857
+ "funding": {
858
+ "url": "https://opencollective.com/libvips"
859
+ },
860
+ "optionalDependencies": {
861
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
862
+ }
863
+ },
864
+ "node_modules/@img/sharp-linux-ppc64": {
865
+ "version": "0.34.5",
866
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
867
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
868
+ "cpu": [
869
+ "ppc64"
870
+ ],
871
+ "dev": true,
872
+ "license": "Apache-2.0",
873
+ "optional": true,
874
+ "os": [
875
+ "linux"
876
+ ],
877
+ "engines": {
878
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
879
+ },
880
+ "funding": {
881
+ "url": "https://opencollective.com/libvips"
882
+ },
883
+ "optionalDependencies": {
884
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
885
+ }
886
+ },
887
+ "node_modules/@img/sharp-linux-riscv64": {
888
+ "version": "0.34.5",
889
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
890
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
891
+ "cpu": [
892
+ "riscv64"
893
+ ],
894
+ "dev": true,
895
+ "license": "Apache-2.0",
896
+ "optional": true,
897
+ "os": [
898
+ "linux"
899
+ ],
900
+ "engines": {
901
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
902
+ },
903
+ "funding": {
904
+ "url": "https://opencollective.com/libvips"
905
+ },
906
+ "optionalDependencies": {
907
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
908
+ }
909
+ },
910
+ "node_modules/@img/sharp-linux-s390x": {
911
+ "version": "0.34.5",
912
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
913
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
914
+ "cpu": [
915
+ "s390x"
916
+ ],
917
+ "dev": true,
918
+ "license": "Apache-2.0",
919
+ "optional": true,
920
+ "os": [
921
+ "linux"
922
+ ],
923
+ "engines": {
924
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
925
+ },
926
+ "funding": {
927
+ "url": "https://opencollective.com/libvips"
928
+ },
929
+ "optionalDependencies": {
930
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
931
+ }
932
+ },
933
+ "node_modules/@img/sharp-linux-x64": {
934
+ "version": "0.34.5",
935
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
936
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
937
+ "cpu": [
938
+ "x64"
939
+ ],
940
+ "dev": true,
941
+ "license": "Apache-2.0",
942
+ "optional": true,
943
+ "os": [
944
+ "linux"
945
+ ],
946
+ "engines": {
947
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
948
+ },
949
+ "funding": {
950
+ "url": "https://opencollective.com/libvips"
951
+ },
952
+ "optionalDependencies": {
953
+ "@img/sharp-libvips-linux-x64": "1.2.4"
954
+ }
955
+ },
956
+ "node_modules/@img/sharp-linuxmusl-arm64": {
957
+ "version": "0.34.5",
958
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
959
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
960
+ "cpu": [
961
+ "arm64"
962
+ ],
963
+ "dev": true,
964
+ "license": "Apache-2.0",
965
+ "optional": true,
966
+ "os": [
967
+ "linux"
968
+ ],
969
+ "engines": {
970
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
971
+ },
972
+ "funding": {
973
+ "url": "https://opencollective.com/libvips"
974
+ },
975
+ "optionalDependencies": {
976
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
977
+ }
978
+ },
979
+ "node_modules/@img/sharp-linuxmusl-x64": {
980
+ "version": "0.34.5",
981
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
982
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
983
+ "cpu": [
984
+ "x64"
985
+ ],
986
+ "dev": true,
987
+ "license": "Apache-2.0",
988
+ "optional": true,
989
+ "os": [
990
+ "linux"
991
+ ],
992
+ "engines": {
993
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
994
+ },
995
+ "funding": {
996
+ "url": "https://opencollective.com/libvips"
997
+ },
998
+ "optionalDependencies": {
999
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
1000
+ }
1001
+ },
1002
+ "node_modules/@img/sharp-wasm32": {
1003
+ "version": "0.34.5",
1004
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
1005
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
1006
+ "cpu": [
1007
+ "wasm32"
1008
+ ],
1009
+ "dev": true,
1010
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
1011
+ "optional": true,
1012
+ "dependencies": {
1013
+ "@emnapi/runtime": "^1.7.0"
1014
+ },
1015
+ "engines": {
1016
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1017
+ },
1018
+ "funding": {
1019
+ "url": "https://opencollective.com/libvips"
1020
+ }
1021
+ },
1022
+ "node_modules/@img/sharp-win32-arm64": {
1023
+ "version": "0.34.5",
1024
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
1025
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
1026
+ "cpu": [
1027
+ "arm64"
1028
+ ],
1029
+ "dev": true,
1030
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
1031
+ "optional": true,
1032
+ "os": [
1033
+ "win32"
1034
+ ],
1035
+ "engines": {
1036
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1037
+ },
1038
+ "funding": {
1039
+ "url": "https://opencollective.com/libvips"
1040
+ }
1041
+ },
1042
+ "node_modules/@img/sharp-win32-ia32": {
1043
+ "version": "0.34.5",
1044
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
1045
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
1046
+ "cpu": [
1047
+ "ia32"
1048
+ ],
1049
+ "dev": true,
1050
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
1051
+ "optional": true,
1052
+ "os": [
1053
+ "win32"
1054
+ ],
1055
+ "engines": {
1056
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1057
+ },
1058
+ "funding": {
1059
+ "url": "https://opencollective.com/libvips"
1060
+ }
1061
+ },
1062
+ "node_modules/@img/sharp-win32-x64": {
1063
+ "version": "0.34.5",
1064
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
1065
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
1066
+ "cpu": [
1067
+ "x64"
1068
+ ],
1069
+ "dev": true,
1070
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
1071
+ "optional": true,
1072
+ "os": [
1073
+ "win32"
1074
+ ],
1075
+ "engines": {
1076
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1077
+ },
1078
+ "funding": {
1079
+ "url": "https://opencollective.com/libvips"
1080
+ }
1081
+ },
1082
+ "node_modules/@jridgewell/resolve-uri": {
1083
+ "version": "3.1.2",
1084
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
1085
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
1086
+ "dev": true,
1087
+ "license": "MIT",
1088
+ "engines": {
1089
+ "node": ">=6.0.0"
1090
+ }
1091
+ },
1092
+ "node_modules/@jridgewell/sourcemap-codec": {
1093
+ "version": "1.5.5",
1094
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
1095
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
1096
+ "dev": true,
1097
+ "license": "MIT"
1098
+ },
1099
+ "node_modules/@jridgewell/trace-mapping": {
1100
+ "version": "0.3.9",
1101
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
1102
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
1103
+ "dev": true,
1104
+ "license": "MIT",
1105
+ "dependencies": {
1106
+ "@jridgewell/resolve-uri": "^3.0.3",
1107
+ "@jridgewell/sourcemap-codec": "^1.4.10"
1108
+ }
1109
+ },
1110
+ "node_modules/@poppinss/colors": {
1111
+ "version": "4.1.6",
1112
+ "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
1113
+ "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",
1114
+ "dev": true,
1115
+ "license": "MIT",
1116
+ "dependencies": {
1117
+ "kleur": "^4.1.5"
1118
+ }
1119
+ },
1120
+ "node_modules/@poppinss/dumper": {
1121
+ "version": "0.6.5",
1122
+ "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",
1123
+ "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",
1124
+ "dev": true,
1125
+ "license": "MIT",
1126
+ "dependencies": {
1127
+ "@poppinss/colors": "^4.1.5",
1128
+ "@sindresorhus/is": "^7.0.2",
1129
+ "supports-color": "^10.0.0"
1130
+ }
1131
+ },
1132
+ "node_modules/@poppinss/exception": {
1133
+ "version": "1.2.3",
1134
+ "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",
1135
+ "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",
1136
+ "dev": true,
1137
+ "license": "MIT"
1138
+ },
1139
+ "node_modules/@sindresorhus/is": {
1140
+ "version": "7.2.0",
1141
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
1142
+ "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
1143
+ "dev": true,
1144
+ "license": "MIT",
1145
+ "engines": {
1146
+ "node": ">=18"
1147
+ },
1148
+ "funding": {
1149
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
1150
+ }
1151
+ },
1152
+ "node_modules/@speed-highlight/core": {
1153
+ "version": "1.2.17",
1154
+ "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz",
1155
+ "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==",
1156
+ "dev": true,
1157
+ "license": "CC0-1.0"
1158
+ },
1159
+ "node_modules/ansi-regex": {
1160
+ "version": "5.0.1",
1161
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
1162
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1163
+ "dev": true,
1164
+ "license": "MIT",
1165
+ "engines": {
1166
+ "node": ">=8"
1167
+ }
1168
+ },
1169
+ "node_modules/ansi-styles": {
1170
+ "version": "4.3.0",
1171
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
1172
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
1173
+ "dev": true,
1174
+ "license": "MIT",
1175
+ "dependencies": {
1176
+ "color-convert": "^2.0.1"
1177
+ },
1178
+ "engines": {
1179
+ "node": ">=8"
1180
+ },
1181
+ "funding": {
1182
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1183
+ }
1184
+ },
1185
+ "node_modules/axios": {
1186
+ "version": "0.21.4",
1187
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
1188
+ "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
1189
+ "dev": true,
1190
+ "license": "MIT",
1191
+ "dependencies": {
1192
+ "follow-redirects": "^1.14.0"
1193
+ }
1194
+ },
1195
+ "node_modules/blake3-wasm": {
1196
+ "version": "2.1.5",
1197
+ "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
1198
+ "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
1199
+ "dev": true,
1200
+ "license": "MIT"
1201
+ },
1202
+ "node_modules/cliui": {
1203
+ "version": "7.0.4",
1204
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
1205
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
1206
+ "dev": true,
1207
+ "license": "ISC",
1208
+ "dependencies": {
1209
+ "string-width": "^4.2.0",
1210
+ "strip-ansi": "^6.0.0",
1211
+ "wrap-ansi": "^7.0.0"
1212
+ }
1213
+ },
1214
+ "node_modules/color-convert": {
1215
+ "version": "2.0.1",
1216
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1217
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1218
+ "dev": true,
1219
+ "license": "MIT",
1220
+ "dependencies": {
1221
+ "color-name": "~1.1.4"
1222
+ },
1223
+ "engines": {
1224
+ "node": ">=7.0.0"
1225
+ }
1226
+ },
1227
+ "node_modules/color-name": {
1228
+ "version": "1.1.4",
1229
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1230
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1231
+ "dev": true,
1232
+ "license": "MIT"
1233
+ },
1234
+ "node_modules/cookie": {
1235
+ "version": "1.1.1",
1236
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
1237
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
1238
+ "dev": true,
1239
+ "license": "MIT",
1240
+ "engines": {
1241
+ "node": ">=18"
1242
+ },
1243
+ "funding": {
1244
+ "type": "opencollective",
1245
+ "url": "https://opencollective.com/express"
1246
+ }
1247
+ },
1248
+ "node_modules/debug": {
1249
+ "version": "4.3.2",
1250
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
1251
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
1252
+ "dev": true,
1253
+ "license": "MIT",
1254
+ "dependencies": {
1255
+ "ms": "2.1.2"
1256
+ },
1257
+ "engines": {
1258
+ "node": ">=6.0"
1259
+ },
1260
+ "peerDependenciesMeta": {
1261
+ "supports-color": {
1262
+ "optional": true
1263
+ }
1264
+ }
1265
+ },
1266
+ "node_modules/detect-libc": {
1267
+ "version": "2.1.2",
1268
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
1269
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
1270
+ "dev": true,
1271
+ "license": "Apache-2.0",
1272
+ "engines": {
1273
+ "node": ">=8"
1274
+ }
1275
+ },
1276
+ "node_modules/emoji-regex": {
1277
+ "version": "8.0.0",
1278
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
1279
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
1280
+ "dev": true,
1281
+ "license": "MIT"
1282
+ },
1283
+ "node_modules/error-stack-parser-es": {
1284
+ "version": "1.0.5",
1285
+ "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
1286
+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
1287
+ "dev": true,
1288
+ "license": "MIT",
1289
+ "funding": {
1290
+ "url": "https://github.com/sponsors/antfu"
1291
+ }
1292
+ },
1293
+ "node_modules/esbuild": {
1294
+ "version": "0.28.1",
1295
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
1296
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
1297
+ "dev": true,
1298
+ "hasInstallScript": true,
1299
+ "license": "MIT",
1300
+ "bin": {
1301
+ "esbuild": "bin/esbuild"
1302
+ },
1303
+ "engines": {
1304
+ "node": ">=18"
1305
+ },
1306
+ "optionalDependencies": {
1307
+ "@esbuild/aix-ppc64": "0.28.1",
1308
+ "@esbuild/android-arm": "0.28.1",
1309
+ "@esbuild/android-arm64": "0.28.1",
1310
+ "@esbuild/android-x64": "0.28.1",
1311
+ "@esbuild/darwin-arm64": "0.28.1",
1312
+ "@esbuild/darwin-x64": "0.28.1",
1313
+ "@esbuild/freebsd-arm64": "0.28.1",
1314
+ "@esbuild/freebsd-x64": "0.28.1",
1315
+ "@esbuild/linux-arm": "0.28.1",
1316
+ "@esbuild/linux-arm64": "0.28.1",
1317
+ "@esbuild/linux-ia32": "0.28.1",
1318
+ "@esbuild/linux-loong64": "0.28.1",
1319
+ "@esbuild/linux-mips64el": "0.28.1",
1320
+ "@esbuild/linux-ppc64": "0.28.1",
1321
+ "@esbuild/linux-riscv64": "0.28.1",
1322
+ "@esbuild/linux-s390x": "0.28.1",
1323
+ "@esbuild/linux-x64": "0.28.1",
1324
+ "@esbuild/netbsd-arm64": "0.28.1",
1325
+ "@esbuild/netbsd-x64": "0.28.1",
1326
+ "@esbuild/openbsd-arm64": "0.28.1",
1327
+ "@esbuild/openbsd-x64": "0.28.1",
1328
+ "@esbuild/openharmony-arm64": "0.28.1",
1329
+ "@esbuild/sunos-x64": "0.28.1",
1330
+ "@esbuild/win32-arm64": "0.28.1",
1331
+ "@esbuild/win32-ia32": "0.28.1",
1332
+ "@esbuild/win32-x64": "0.28.1"
1333
+ }
1334
+ },
1335
+ "node_modules/escalade": {
1336
+ "version": "3.2.0",
1337
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1338
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1339
+ "dev": true,
1340
+ "license": "MIT",
1341
+ "engines": {
1342
+ "node": ">=6"
1343
+ }
1344
+ },
1345
+ "node_modules/follow-redirects": {
1346
+ "version": "1.16.0",
1347
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
1348
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
1349
+ "dev": true,
1350
+ "funding": [
1351
+ {
1352
+ "type": "individual",
1353
+ "url": "https://github.com/sponsors/RubenVerborgh"
1354
+ }
1355
+ ],
1356
+ "license": "MIT",
1357
+ "engines": {
1358
+ "node": ">=4.0"
1359
+ },
1360
+ "peerDependenciesMeta": {
1361
+ "debug": {
1362
+ "optional": true
1363
+ }
1364
+ }
1365
+ },
1366
+ "node_modules/fsevents": {
1367
+ "version": "2.3.3",
1368
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1369
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1370
+ "dev": true,
1371
+ "hasInstallScript": true,
1372
+ "license": "MIT",
1373
+ "optional": true,
1374
+ "os": [
1375
+ "darwin"
1376
+ ],
1377
+ "engines": {
1378
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1379
+ }
1380
+ },
1381
+ "node_modules/get-caller-file": {
1382
+ "version": "2.0.5",
1383
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
1384
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
1385
+ "dev": true,
1386
+ "license": "ISC",
1387
+ "engines": {
1388
+ "node": "6.* || 8.* || >= 10.*"
1389
+ }
1390
+ },
1391
+ "node_modules/is-fullwidth-code-point": {
1392
+ "version": "3.0.0",
1393
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
1394
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
1395
+ "dev": true,
1396
+ "license": "MIT",
1397
+ "engines": {
1398
+ "node": ">=8"
1399
+ }
1400
+ },
1401
+ "node_modules/kleur": {
1402
+ "version": "4.1.5",
1403
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
1404
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
1405
+ "dev": true,
1406
+ "license": "MIT",
1407
+ "engines": {
1408
+ "node": ">=6"
1409
+ }
1410
+ },
1411
+ "node_modules/localtunnel": {
1412
+ "version": "2.0.2",
1413
+ "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz",
1414
+ "integrity": "sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==",
1415
+ "dev": true,
1416
+ "license": "MIT",
1417
+ "dependencies": {
1418
+ "axios": "0.21.4",
1419
+ "debug": "4.3.2",
1420
+ "openurl": "1.1.1",
1421
+ "yargs": "17.1.1"
1422
+ },
1423
+ "bin": {
1424
+ "lt": "bin/lt.js"
1425
+ },
1426
+ "engines": {
1427
+ "node": ">=8.3.0"
1428
+ }
1429
+ },
1430
+ "node_modules/miniflare": {
1431
+ "version": "4.20260710.0",
1432
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260710.0.tgz",
1433
+ "integrity": "sha512-x1LLRkU6o1p7hiKrB0TRnL0MJn6xFOT+/vrlEQINz5cRDKLP8ru4hBqWTIvXAetzr1acKAnmAaG84pQ4W/K14g==",
1434
+ "dev": true,
1435
+ "license": "MIT",
1436
+ "dependencies": {
1437
+ "@cspotcode/source-map-support": "0.8.1",
1438
+ "sharp": "0.34.5",
1439
+ "undici": "7.28.0",
1440
+ "workerd": "1.20260710.1",
1441
+ "ws": "8.21.0",
1442
+ "youch": "4.1.0-beta.10"
1443
+ },
1444
+ "bin": {
1445
+ "miniflare": "bootstrap.js"
1446
+ },
1447
+ "engines": {
1448
+ "node": ">=22.0.0"
1449
+ }
1450
+ },
1451
+ "node_modules/ms": {
1452
+ "version": "2.1.2",
1453
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1454
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
1455
+ "dev": true,
1456
+ "license": "MIT"
1457
+ },
1458
+ "node_modules/openurl": {
1459
+ "version": "1.1.1",
1460
+ "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz",
1461
+ "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==",
1462
+ "dev": true,
1463
+ "license": "MIT"
1464
+ },
1465
+ "node_modules/path-to-regexp": {
1466
+ "version": "6.3.0",
1467
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
1468
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
1469
+ "dev": true,
1470
+ "license": "MIT"
1471
+ },
1472
+ "node_modules/pathe": {
1473
+ "version": "2.0.3",
1474
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
1475
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
1476
+ "dev": true,
1477
+ "license": "MIT"
1478
+ },
1479
+ "node_modules/require-directory": {
1480
+ "version": "2.1.1",
1481
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
1482
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
1483
+ "dev": true,
1484
+ "license": "MIT",
1485
+ "engines": {
1486
+ "node": ">=0.10.0"
1487
+ }
1488
+ },
1489
+ "node_modules/semver": {
1490
+ "version": "7.8.5",
1491
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
1492
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
1493
+ "dev": true,
1494
+ "license": "ISC",
1495
+ "bin": {
1496
+ "semver": "bin/semver.js"
1497
+ },
1498
+ "engines": {
1499
+ "node": ">=10"
1500
+ }
1501
+ },
1502
+ "node_modules/sharp": {
1503
+ "version": "0.34.5",
1504
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
1505
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
1506
+ "dev": true,
1507
+ "hasInstallScript": true,
1508
+ "license": "Apache-2.0",
1509
+ "dependencies": {
1510
+ "@img/colour": "^1.0.0",
1511
+ "detect-libc": "^2.1.2",
1512
+ "semver": "^7.7.3"
1513
+ },
1514
+ "engines": {
1515
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1516
+ },
1517
+ "funding": {
1518
+ "url": "https://opencollective.com/libvips"
1519
+ },
1520
+ "optionalDependencies": {
1521
+ "@img/sharp-darwin-arm64": "0.34.5",
1522
+ "@img/sharp-darwin-x64": "0.34.5",
1523
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
1524
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
1525
+ "@img/sharp-libvips-linux-arm": "1.2.4",
1526
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
1527
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
1528
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
1529
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
1530
+ "@img/sharp-libvips-linux-x64": "1.2.4",
1531
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
1532
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
1533
+ "@img/sharp-linux-arm": "0.34.5",
1534
+ "@img/sharp-linux-arm64": "0.34.5",
1535
+ "@img/sharp-linux-ppc64": "0.34.5",
1536
+ "@img/sharp-linux-riscv64": "0.34.5",
1537
+ "@img/sharp-linux-s390x": "0.34.5",
1538
+ "@img/sharp-linux-x64": "0.34.5",
1539
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
1540
+ "@img/sharp-linuxmusl-x64": "0.34.5",
1541
+ "@img/sharp-wasm32": "0.34.5",
1542
+ "@img/sharp-win32-arm64": "0.34.5",
1543
+ "@img/sharp-win32-ia32": "0.34.5",
1544
+ "@img/sharp-win32-x64": "0.34.5"
1545
+ }
1546
+ },
1547
+ "node_modules/string-width": {
1548
+ "version": "4.2.3",
1549
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
1550
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
1551
+ "dev": true,
1552
+ "license": "MIT",
1553
+ "dependencies": {
1554
+ "emoji-regex": "^8.0.0",
1555
+ "is-fullwidth-code-point": "^3.0.0",
1556
+ "strip-ansi": "^6.0.1"
1557
+ },
1558
+ "engines": {
1559
+ "node": ">=8"
1560
+ }
1561
+ },
1562
+ "node_modules/strip-ansi": {
1563
+ "version": "6.0.1",
1564
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
1565
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1566
+ "dev": true,
1567
+ "license": "MIT",
1568
+ "dependencies": {
1569
+ "ansi-regex": "^5.0.1"
1570
+ },
1571
+ "engines": {
1572
+ "node": ">=8"
1573
+ }
1574
+ },
1575
+ "node_modules/supports-color": {
1576
+ "version": "10.2.2",
1577
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
1578
+ "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
1579
+ "dev": true,
1580
+ "license": "MIT",
1581
+ "engines": {
1582
+ "node": ">=18"
1583
+ },
1584
+ "funding": {
1585
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
1586
+ }
1587
+ },
1588
+ "node_modules/tslib": {
1589
+ "version": "2.8.1",
1590
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1591
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1592
+ "dev": true,
1593
+ "license": "0BSD",
1594
+ "optional": true
1595
+ },
1596
+ "node_modules/undici": {
1597
+ "version": "7.28.0",
1598
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
1599
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
1600
+ "dev": true,
1601
+ "license": "MIT",
1602
+ "engines": {
1603
+ "node": ">=20.18.1"
1604
+ }
1605
+ },
1606
+ "node_modules/unenv": {
1607
+ "version": "2.0.0-rc.24",
1608
+ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
1609
+ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
1610
+ "dev": true,
1611
+ "license": "MIT",
1612
+ "peer": true,
1613
+ "dependencies": {
1614
+ "pathe": "^2.0.3"
1615
+ }
1616
+ },
1617
+ "node_modules/workerd": {
1618
+ "version": "1.20260710.1",
1619
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260710.1.tgz",
1620
+ "integrity": "sha512-U2sBPPrb9U97sBKnnMN6Kv8p65903P35nwMkPE9vSH/bRuRqkZ3a1EjUw3jV28RhiyXpkLF77Evzw8XimFxyTw==",
1621
+ "dev": true,
1622
+ "hasInstallScript": true,
1623
+ "license": "Apache-2.0",
1624
+ "peer": true,
1625
+ "bin": {
1626
+ "workerd": "bin/workerd"
1627
+ },
1628
+ "engines": {
1629
+ "node": ">=16"
1630
+ },
1631
+ "optionalDependencies": {
1632
+ "@cloudflare/workerd-darwin-64": "1.20260710.1",
1633
+ "@cloudflare/workerd-darwin-arm64": "1.20260710.1",
1634
+ "@cloudflare/workerd-linux-64": "1.20260710.1",
1635
+ "@cloudflare/workerd-linux-arm64": "1.20260710.1",
1636
+ "@cloudflare/workerd-windows-64": "1.20260710.1"
1637
+ }
1638
+ },
1639
+ "node_modules/wrangler": {
1640
+ "version": "4.111.0",
1641
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.111.0.tgz",
1642
+ "integrity": "sha512-bffpI9EyrnpKkF/1S+RaIv8oRD93GtbsA7TlfWwOsGJGB7VO3jVbdGzpC9TU7Bqom3z7jUxcte4Z9MPhaQ4HoQ==",
1643
+ "dev": true,
1644
+ "license": "MIT OR Apache-2.0",
1645
+ "dependencies": {
1646
+ "@cloudflare/kv-asset-handler": "0.5.0",
1647
+ "@cloudflare/unenv-preset": "2.16.1",
1648
+ "blake3-wasm": "2.1.5",
1649
+ "esbuild": "0.28.1",
1650
+ "miniflare": "4.20260710.0",
1651
+ "path-to-regexp": "6.3.0",
1652
+ "unenv": "2.0.0-rc.24",
1653
+ "workerd": "1.20260710.1"
1654
+ },
1655
+ "bin": {
1656
+ "cf-wrangler": "bin/cf-wrangler.js",
1657
+ "wrangler": "bin/wrangler.js",
1658
+ "wrangler2": "bin/wrangler.js"
1659
+ },
1660
+ "engines": {
1661
+ "node": ">=22.0.0"
1662
+ },
1663
+ "optionalDependencies": {
1664
+ "fsevents": "2.3.3"
1665
+ },
1666
+ "peerDependencies": {
1667
+ "@cloudflare/workers-types": "^5.20260710.1"
1668
+ },
1669
+ "peerDependenciesMeta": {
1670
+ "@cloudflare/workers-types": {
1671
+ "optional": true
1672
+ }
1673
+ }
1674
+ },
1675
+ "node_modules/wrap-ansi": {
1676
+ "version": "7.0.0",
1677
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
1678
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
1679
+ "dev": true,
1680
+ "license": "MIT",
1681
+ "dependencies": {
1682
+ "ansi-styles": "^4.0.0",
1683
+ "string-width": "^4.1.0",
1684
+ "strip-ansi": "^6.0.0"
1685
+ },
1686
+ "engines": {
1687
+ "node": ">=10"
1688
+ },
1689
+ "funding": {
1690
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1691
+ }
1692
+ },
1693
+ "node_modules/ws": {
1694
+ "version": "8.21.0",
1695
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
1696
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
1697
+ "dev": true,
1698
+ "license": "MIT",
1699
+ "engines": {
1700
+ "node": ">=10.0.0"
1701
+ },
1702
+ "peerDependencies": {
1703
+ "bufferutil": "^4.0.1",
1704
+ "utf-8-validate": ">=5.0.2"
1705
+ },
1706
+ "peerDependenciesMeta": {
1707
+ "bufferutil": {
1708
+ "optional": true
1709
+ },
1710
+ "utf-8-validate": {
1711
+ "optional": true
1712
+ }
1713
+ }
1714
+ },
1715
+ "node_modules/y18n": {
1716
+ "version": "5.0.8",
1717
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
1718
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
1719
+ "dev": true,
1720
+ "license": "ISC",
1721
+ "engines": {
1722
+ "node": ">=10"
1723
+ }
1724
+ },
1725
+ "node_modules/yargs": {
1726
+ "version": "17.1.1",
1727
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz",
1728
+ "integrity": "sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==",
1729
+ "dev": true,
1730
+ "license": "MIT",
1731
+ "dependencies": {
1732
+ "cliui": "^7.0.2",
1733
+ "escalade": "^3.1.1",
1734
+ "get-caller-file": "^2.0.5",
1735
+ "require-directory": "^2.1.1",
1736
+ "string-width": "^4.2.0",
1737
+ "y18n": "^5.0.5",
1738
+ "yargs-parser": "^20.2.2"
1739
+ },
1740
+ "engines": {
1741
+ "node": ">=12"
1742
+ }
1743
+ },
1744
+ "node_modules/yargs-parser": {
1745
+ "version": "20.2.9",
1746
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
1747
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
1748
+ "dev": true,
1749
+ "license": "ISC",
1750
+ "engines": {
1751
+ "node": ">=10"
1752
+ }
1753
+ },
1754
+ "node_modules/youch": {
1755
+ "version": "4.1.0-beta.10",
1756
+ "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
1757
+ "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
1758
+ "dev": true,
1759
+ "license": "MIT",
1760
+ "dependencies": {
1761
+ "@poppinss/colors": "^4.1.5",
1762
+ "@poppinss/dumper": "^0.6.4",
1763
+ "@speed-highlight/core": "^1.2.7",
1764
+ "cookie": "^1.0.2",
1765
+ "youch-core": "^0.3.3"
1766
+ }
1767
+ },
1768
+ "node_modules/youch-core": {
1769
+ "version": "0.3.3",
1770
+ "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
1771
+ "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
1772
+ "dev": true,
1773
+ "license": "MIT",
1774
+ "dependencies": {
1775
+ "@poppinss/exception": "^1.2.2",
1776
+ "error-stack-parser-es": "^1.0.5"
1777
+ }
1778
+ }
1779
+ }
1780
+ }
package.json CHANGED
@@ -9,6 +9,7 @@
9
  "tail": "wrangler tail"
10
  },
11
  "devDependencies": {
 
12
  "wrangler": "^4.110.0"
13
  }
14
  }
 
9
  "tail": "wrangler tail"
10
  },
11
  "devDependencies": {
12
+ "localtunnel": "^2.0.2",
13
  "wrangler": "^4.110.0"
14
  }
15
  }
start-relay.ps1 ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # start-relay.ps1
2
+ # הרץ את הסקריפט הזה מ-PowerShell שלך (לא דרך כלי אוטומציה).
3
+ # התהליכים ישארו חיים ברקע עד שתכבה את המחשב.
4
+ #
5
+ # שימוש:
6
+ # $env:CLOUDFLARE_API_TOKEN = "cfut_YOUR_TOKEN_HERE"
7
+ # .\start-relay.ps1
8
+ #
9
+ # לעצירה: .\stop-relay.ps1
10
+
11
+ param()
12
+ Set-StrictMode -Off
13
+ $ErrorActionPreference = "Continue"
14
+
15
+ $RelayDir = Join-Path $PSScriptRoot "home-relay"
16
+ $WranglerDir = $PSScriptRoot
17
+ $CloudflaredExe = "C:\Program Files (x86)\cloudflared\cloudflared.exe"
18
+
19
+ # RELAY_SECRET — never hardcode it here (this file is committed). Read from the
20
+ # User-persisted env var; generate a strong random one on first run and save it.
21
+ # Step 5 pushes this same value to the Worker (YT_RELAY_SECRET) so the two stay in sync.
22
+ $Secret = $env:RELAY_SECRET
23
+ if (-not $Secret) { $Secret = [Environment]::GetEnvironmentVariable("RELAY_SECRET", "User") }
24
+ if (-not $Secret) {
25
+ $bytes = New-Object 'System.Byte[]' 32
26
+ [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
27
+ $Secret = (($bytes | ForEach-Object { $_.ToString("x2") }) -join "")
28
+ [Environment]::SetEnvironmentVariable("RELAY_SECRET", $Secret, "User")
29
+ Write-Host "Generated a new RELAY_SECRET and saved it to your User environment." -ForegroundColor Yellow
30
+ Write-Host "NOTE: set CLOUDFLARE_API_TOKEN this run so step 5 pushes it to the Worker." -ForegroundColor Yellow
31
+ }
32
+
33
+ $RelayLog = Join-Path $env:TEMP "relay.log"
34
+ $RelayErrLog = Join-Path $env:TEMP "relay-error.log"
35
+ $CfLog = Join-Path $env:TEMP "cloudflared-relay.log"
36
+ $UrlFile = Join-Path $env:TEMP "yt-relay-url.txt"
37
+
38
+ # ── 1. Kill old processes ─────────────────────────────────────────────────────
39
+ Write-Host "[1/5] Stopping old processes..." -ForegroundColor Yellow
40
+ Get-Process -Name "cloudflared" -ErrorAction SilentlyContinue | Stop-Process -Force
41
+ Get-CimInstance Win32_Process -Filter "Name='node.exe' AND CommandLine LIKE '%server.js%'" |
42
+ ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
43
+ Start-Sleep -Seconds 2
44
+ Remove-Item $RelayLog,$RelayErrLog,$CfLog,$UrlFile -ErrorAction SilentlyContinue
45
+
46
+ # ── 2. Start Node relay (truly detached — no stdout redirect) ─────────────────
47
+ Write-Host "[2/5] Starting home relay (node)..." -ForegroundColor Cyan
48
+ $env:RELAY_SECRET = $Secret
49
+ $env:PORT = "8788"
50
+ $env:HOST = "127.0.0.1"
51
+ # If Node fails with CERT_HAS_EXPIRED / UNABLE_TO_VERIFY — filter MITM on YouTube TLS
52
+ if (-not $env:RELAY_INSECURE_TLS) {
53
+ $env:RELAY_INSECURE_TLS = [Environment]::GetEnvironmentVariable("RELAY_INSECURE_TLS", "User")
54
+ }
55
+ # Start-Process WITHOUT -RedirectStandardOutput = truly detached child process
56
+ # Pass env via cmd so child inherits RELAY_* cleanly
57
+ $nodeArgs = "server.js"
58
+ Start-Process -FilePath "node" `
59
+ -ArgumentList $nodeArgs `
60
+ -WorkingDirectory $RelayDir `
61
+ -WindowStyle Hidden
62
+
63
+ # ── 3. Wait for /health ───────────────────────────────────────────────────────
64
+ Write-Host "[3/5] Waiting for /health on :8788..." -ForegroundColor Cyan
65
+ $ok = $false
66
+ for ($i = 1; $i -le 20; $i++) {
67
+ Start-Sleep -Seconds 1
68
+ try {
69
+ $r = Invoke-RestMethod "http://127.0.0.1:8788/health" -TimeoutSec 2 -ErrorAction Stop
70
+ if ($r.ok) { $ok = $true; break }
71
+ } catch {}
72
+ Write-Host " ...($i/20)"
73
+ }
74
+ if (-not $ok) {
75
+ Write-Host "ERROR: relay did not come up on :8788" -ForegroundColor Red
76
+ exit 1
77
+ }
78
+ Write-Host "OK: relay is up." -ForegroundColor Green
79
+
80
+ # ── 4. Start cloudflared (truly detached) ────────────────────────────────────
81
+ Write-Host "[4/5] Starting cloudflared tunnel..." -ForegroundColor Cyan
82
+ Remove-Item $CfLog -ErrorAction SilentlyContinue
83
+ Start-Process -FilePath $CloudflaredExe `
84
+ -ArgumentList "tunnel","--protocol","http2","--url","http://127.0.0.1:8788","--logfile",$CfLog `
85
+ -WindowStyle Hidden
86
+
87
+ $tunnelUrl = $null
88
+ for ($i = 1; $i -le 25; $i++) {
89
+ Start-Sleep -Seconds 1
90
+ if (Test-Path $CfLog) {
91
+ $txt = Get-Content $CfLog -Raw -ErrorAction SilentlyContinue
92
+ if ($txt -match 'https://[a-z0-9-]+\.trycloudflare\.com') {
93
+ $tunnelUrl = $Matches[0]; break
94
+ }
95
+ }
96
+ Write-Host " ...waiting for tunnel URL ($i/25)"
97
+ }
98
+
99
+ if (-not $tunnelUrl) {
100
+ Write-Host "ERROR: no tunnel URL. Check: $CfLog" -ForegroundColor Red
101
+ Get-Content $CfLog -Tail 10 -ErrorAction SilentlyContinue
102
+ exit 1
103
+ }
104
+
105
+ $tunnelUrl | Set-Content $UrlFile
106
+ Write-Host "OK: tunnel = $tunnelUrl" -ForegroundColor Green
107
+
108
+ # ── 5. Update Worker secrets ─────────────────────────────────────────���────────
109
+ Write-Host "[5/5] Updating Cloudflare Worker secrets..." -ForegroundColor Cyan
110
+ if (-not $env:CLOUDFLARE_API_TOKEN) {
111
+ Write-Host "WARNING: CLOUDFLARE_API_TOKEN not set - skipping Worker update." -ForegroundColor Yellow
112
+ Write-Host " Run: echo '$tunnelUrl' | npx wrangler secret put YT_RELAY_URL" -ForegroundColor Gray
113
+ } else {
114
+ Push-Location $WranglerDir
115
+ $tunnelUrl | npx wrangler secret put YT_RELAY_URL
116
+ $Secret | npx wrangler secret put YT_RELAY_SECRET
117
+ Pop-Location
118
+ }
119
+
120
+ Write-Host ""
121
+ Write-Host "================================================" -ForegroundColor Green
122
+ Write-Host " SUCCESS - relay + tunnel running in background" -ForegroundColor Green
123
+ Write-Host " Tunnel : $tunnelUrl" -ForegroundColor Green
124
+ Write-Host " Relay : http://127.0.0.1:8788/health" -ForegroundColor Green
125
+ Write-Host " Stop : .\stop-relay.ps1" -ForegroundColor Green
126
+ Write-Host "================================================" -ForegroundColor Green
start_bypass_relay.vbs ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ Set WshShell = CreateObject("WScript.Shell")
2
+ WshShell.Run "powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File ""C:\Users\user\Documents\מעקף פרוקסי 2.0\start-relay.ps1""", 0, False
stop-relay.ps1 ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # stop-relay.ps1 - Stops Node relay and cloudflared Scheduled Tasks
2
+
3
+ Write-Host "Stopping EtrogRelay and EtrogTunnel tasks..." -ForegroundColor Yellow
4
+ schtasks /end /tn "EtrogRelay" 2>$null | Out-Null
5
+ schtasks /end /tn "EtrogTunnel" 2>$null | Out-Null
6
+ schtasks /delete /tn "EtrogRelay" /f 2>$null | Out-Null
7
+ schtasks /delete /tn "EtrogTunnel" /f 2>$null | Out-Null
8
+ Get-Process -Name "cloudflared" -ErrorAction SilentlyContinue | Stop-Process -Force
9
+ Get-CimInstance Win32_Process -Filter "Name='node.exe' AND CommandLine LIKE '%server.js%'" |
10
+ ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }
11
+ Write-Host "Done." -ForegroundColor Green
worker.js CHANGED
@@ -7,16 +7,78 @@
7
 
8
  const PROXY_PATH = '/cdn/';
9
  const KEY = [97, 110, 116, 105, 103, 114, 97, 118, 105, 116, 121]; // "antigravity"
10
- const SW_VERSION = 97;
11
 
12
  // ── Access gate (password) ──────────────────────────────────────────────────
13
  // Session cookie only (no Max-Age) → cleared when the browser is closed.
14
  const GATE_PASSWORD = 'מוישי';
15
  const GATE_COOKIE = '__proxy_gate';
16
  const GATE_PEPPER = 'etrog-gate-v1-antigravity'; // used only to HMAC the session token
 
 
 
17
 
18
- // Built-in YouTube cookies (from user cookies.txt) — always available
19
- const BUILTIN_YOUTUBE_COOKIE = "LOGIN_INFO=AFmmF2swRAIgOG8sdpkCPpH5qrWLOJre1oBYXaN6786g2yLudsx-ERsCIDaMfFg-v8we_C7Nsw4fYoZgDR1Zv5BA2-tDzkQPmDKN:QUQ3MjNmeEpDVGhFUUlPbXFtN2ZNS3hKNkc3VlB5dTRVRHNvYVlpNzNrU2tjallGdXRzZlkzS0NCcHZUNHc2QUNxTXNfeWZvU09MajRjWFV4VE5KUDBhNWVnR3ZiMG00ZDU2MFQyWXF4RDhOWk80bFRLc3VBRlF6NHBQUjlfSGc0bGd0NkxieFQ1R2t6cnRid1d1VzNiX3NqVDVEYWpTXzVR; __Secure-YENID=16.YTE=vUVwcBrwqrh9dHwLyT2Iee4UvjHi6cbOxlFiqNbRcZoOb9leeRAv8pwH6CpiyO3aOG5zUjYq-1-od8Any5ic_1mlpfhOakad-mekIiQXOHPb3hJTkilk_F8MkZNdLb0UatxpPeZQ_ClGgt2jwx94859bZaAQdtDM41qUhlRYO6f2l3u69UiQJNvBQmNdjJNHfo7QnupRsOg-KZWuBu936DosufDu_NUALoO4lgH4YnzBG85alAEUH5utV_b83G9As3ZWpLdmSwbLWA6944R977eDGc2lB1VTHDP67s1F5z-yqtHkl3LtOEKYuAVGWaQqXVtTN6k_kkMOuxh5-OV-8A; VISITOR_INFO1_LIVE=CdhV9Fm2uW0; VISITOR_PRIVACY_METADATA=CgJJTBIEGgAgRQ%3D%3D; SID=g.a000_wiShz57yp_RAOUIJUdjiMQRK74oMiT3hicRAXCRo3vExltlDo4PpKx74Ygu20bVHjAwCgACgYKAYsSARISFQHGX2MidSJSW5So8D8Ji2UnpSE8ihoVAUF8yKoa8Lx0YfRwR1Kt9LNpqSbV0076; __Secure-1PSID=g.a000_wiShz57yp_RAOUIJUdjiMQRK74oMiT3hicRAXCRo3vExltlnNMhzk7Ir67iGpM6btI_WgACgYKAacSARISFQHGX2MiOJNazm--Up7LodT9R3Gd8hoVAUF8yKq7FcEpifvn4Xmr14B2fvvt0076; __Secure-3PSID=g.a000_wiShz57yp_RAOUIJUdjiMQRK74oMiT3hicRAXCRo3vExltlSM9lOIW-RrXxfaIBbuxdIQACgYKAdwSARISFQHGX2Mi0OHeaUt70yxrhuOK_BrnFxoVAUF8yKqyMbnBo0BDfxt3TLLMcLGS0076; HSID=A16gUIUrjykjJvquB; SSID=A-DZIwGa5SOmGskOH; APISID=seam9EHryTHIXW_C/Ai-ENWjhjvltAB3FC; SAPISID=UL6Vh_S22fpeIdM8/AD6Qb4wlcTslni3Dk; __Secure-1PAPISID=UL6Vh_S22fpeIdM8/AD6Qb4wlcTslni3Dk; __Secure-3PAPISID=UL6Vh_S22fpeIdM8/AD6Qb4wlcTslni3Dk; __Secure-YNID=20.YT=mfSyVx7S9-0fvC-4Fd6Z-6QotdeXj03RqxEHJ_AhqjV7PbakeVCMIeNSaGVNDRwHY_uJFNdqYXNua9hqc2o0XtKFWW9oZG-modg3eDQNl2RKP2oKZnhka95cL900NUYtt6Ibh_Kb3NBf28jwmwlpN45KJnb42I1zVI_oyuOhS6j2nPj4sXoA29idQAoC2zWr82gyUf4dyA_uElutXLDwYGv8uQTyUx7URSpSB1k9-VlZyKn4s4Y1JYm-RAP2YsinX_v5W6s4sOxTx5EfWrTNzRBGJtTTIlbETTEGMeomkEqN4KwqV22q9X1Sj1hhvwecIDJCd1wPB1gPhWkxNZqI-Q; __Secure-ROLLOUT_TOKEN=CN7OiKbF28Le4wEQ1fCLirrQlQMYxdydsbvUlQM%3D; YSC=nrmfHve5buM; __Secure-1PSIDTS=sidts-CjUBPWEu2ZsyRswaKGEYXXuInu_3QBs1ghx318R2_5T3loFIK151gIokQBwSEToggO69liodZRAA; __Secure-3PSIDTS=sidts-CjUBPWEu2ZsyRswaKGEYXXuInu_3QBs1ghx318R2_5T3loFIK151gIokQBwSEToggO69liodZRAA; PREF=tz=Asia.Jerusalem&f7=100&f4=4000000; SIDCC=AKEyXzXludlRyX974UsrW53iBaD1CNFsbDoD1-X_yvUdvEEKXBURHEUg8AaMqZcBvuFjuXjb_lY; __Secure-1PSIDCC=AKEyXzV5-4jIT_jCuN-oii0bgLPTNAMnHOBsBVgKSpgikrvWER6hlFpPuYAGu2ApwpZdkYLD7Bk; __Secure-3PSIDCC=AKEyXzUddIiONDJegYPJ38eDn3Y7xzdapx7Xe-l1zDn6FU04oZzyKu3l2rcwgIX86q_HbtwQF4Q";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  async function gateToken() {
22
  const data = new TextEncoder().encode(GATE_PEPPER + '|' + GATE_PASSWORD);
@@ -45,7 +107,11 @@ function getCookieValue(cookieHeader, name) {
45
  return '';
46
  }
47
 
48
- async function isGateOpen(request) {
 
 
 
 
49
  const got = getCookieValue(request.headers.get('cookie') || '', GATE_COOKIE);
50
  if (!got) return false;
51
  const expect = await gateToken();
@@ -149,7 +215,7 @@ async function handleGatePost(request, url) {
149
  });
150
  }
151
 
152
- async function requireGate(request, url) {
153
  // Login endpoint always reachable
154
  if (url.pathname === '/__gate') {
155
  if (request.method === 'POST') return handleGatePost(request, url);
@@ -166,7 +232,7 @@ async function requireGate(request, url) {
166
  },
167
  });
168
  }
169
- if (await isGateOpen(request)) return null; // allowed
170
 
171
  // API / media / XHR: 401 JSON so clients don't get HTML as media
172
  const dest = (request.headers.get('sec-fetch-dest') || '').toLowerCase();
@@ -226,10 +292,10 @@ function cacheSetFormats(videoId, pack) {
226
  FORMAT_CACHE.delete(first);
227
  }
228
  }
229
- async function getPlayerFormatsCached(videoId, request) {
230
  const cached = cacheGetFormats(videoId);
231
  if (cached) return { ...cached, fromCache: true };
232
- const pack = await fetchPlayerFormats(videoId, request);
233
  if (pack && pack.ok) cacheSetFormats(videoId, pack);
234
  return pack;
235
  }
@@ -452,7 +518,7 @@ function sealedStreamResponse(readable, status, origCt, extraHeaders = {}, range
452
  h.set('cache-control', 'no-store');
453
  h.set(
454
  'Access-Control-Expose-Headers',
455
- 'Content-Length, Content-Range, Accept-Ranges, Content-Type, X-Proxy-Obfuscated, X-Original-Content-Type, X-Seal-Offset, X-Etrog-Mode, X-Etrog-Itag, X-Etrog-Bytes'
456
  );
457
  if (contentLength != null && contentLength !== '') h.set('content-length', String(contentLength));
458
  const sealed = sealTransformStream(readable, rangeStart || 0);
@@ -787,7 +853,8 @@ self.addEventListener('fetch', function(e) {
787
 
788
  // 3. Cross-origin requests: proxy through worker
789
  if (requestUrl.origin !== self.location.origin) {
790
- if (requestUrl.searchParams.has('etrog_direct') || requestUrl.href.indexOf('etrog_direct') !== -1) {
 
791
  return;
792
  }
793
  if (shouldBlock(requestUrl.href)) {
@@ -842,7 +909,9 @@ self.addEventListener('fetch', function(e) {
842
  requestUrl.pathname === '/api/i' ||
843
  requestUrl.pathname === '/x/s' ||
844
  requestUrl.pathname === '/x/t' ||
845
- requestUrl.pathname.indexOf('/x/s/') === 0) {
 
 
846
  e.respondWith(
847
  fetch(e.request).then(function(res) {
848
  // Error JSON (502/4xx) — never XOR-decrypt
@@ -854,7 +923,8 @@ self.addEventListener('fetch', function(e) {
854
  var ct = (res.headers.get('content-type') || '').toLowerCase();
855
  // Block Rimon MITM only when response is NOT from our worker markers
856
  if (!mode && !sealed && (rimon.indexOf('BLOCK') !== -1 || srv.indexOf('lighttpd') !== -1)) {
857
- return new Response(JSON.stringify({ error: 'filter_block', hint: 'Etrog/Rimon intercepted stream' }), {
 
858
  status: 502,
859
  headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
860
  });
@@ -1821,9 +1891,9 @@ const UI_HTML = `<!DOCTYPE html>
1821
  return;
1822
  }
1823
  document.cookie = '__proxy_target=' + encodeURIComponent(u.origin) + '; Path=/; max-age=31536000; SameSite=Lax; Secure';
1824
- window.location.href = '/cdn/' + encodeUrl(u.href);
1825
  } catch (e) {
1826
- window.location.href = '/cdn/' + encodeUrl(url);
1827
  }
1828
  }
1829
 
@@ -2149,10 +2219,67 @@ async function importCookiesPost(request) {
2149
  }
2150
  }
2151
 
2152
- // ג”€ג”€ג”€ Section 9: Error / Sorry Page ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€ג”€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2153
 
2154
- function makeSorryResponse(origin, videoId) {
2155
- if (videoId) return youtubeNativePlayerPage(videoId, origin);
2156
  return makeSorryResponsePlain(origin);
2157
  }
2158
  function makeSorryResponsePlain(origin) {
@@ -2423,7 +2550,7 @@ function isAllowedMediaDirectUrl(u) {
2423
  }
2424
  }
2425
 
2426
- function youtubeNativePlayerPage(videoId, origin, title, prefillQ) {
2427
  const safeId = String(videoId || '').replace(/[^\w-]/g, '');
2428
  const q0 = String(prefillQ || '').replace(/[<>&"]/g, '');
2429
  const pageTitle = (title || 'YouTube') + (safeId ? (' — ' + safeId) : '');
@@ -2715,7 +2842,7 @@ function youtubeNativePlayerPage(videoId, origin, title, prefillQ) {
2715
  '</div>' +
2716
  '</div>' +
2717
  '<input id="vid" value="' + safeId.replace(/"/g, '') + '" tabindex="-1"/>' +
2718
- '<script>\n' + PLAYER_CLIENT_JS + '\n</script></body></html>';
2719
 
2720
  return new Response(html, {
2721
  status: 200,
@@ -2807,13 +2934,12 @@ const PLAYER_CLIENT_JS = [
2807
  " function mediaUrl(id,itag,dl,strict,directUrl){",
2808
  " // Stealth path + XOR token only (no clear video id — harder for Etrog URL filters)",
2809
  " var it=String(itag||currentItag||'18');",
2810
- " var payload={v:id,i:it,t:Date.now(),s:strict?1:0};",
 
2811
  " if(directUrl) payload.u=String(directUrl);",
2812
  " var tok=encodeTok(payload);",
2813
- " // Path looks like static asset chunk (less likely to trigger video URL heuristics)",
2814
- " var u='/x/s/'+tok.slice(0,12)+'.bin?d='+encodeURIComponent(tok)+'&i='+encodeURIComponent(it);",
2815
- " if(strict) u+='&strict=1';",
2816
- " if(dl) u+='&dl=1';",
2817
  " return u;",
2818
  " }",
2819
  " // Client-side Innertube via sandboxed iframe (Origin:null). CF Worker IPs get LOGIN_REQUIRED;",
@@ -2875,7 +3001,7 @@ const PLAYER_CLIENT_JS = [
2875
  " }",
2876
  " function ensureSw(){",
2877
  " if(!('serviceWorker' in navigator)) return Promise.resolve();",
2878
- " return navigator.serviceWorker.register('/sw.js?v=97',{scope:'/'}).then(function(){",
2879
  " return navigator.serviceWorker.ready;",
2880
  " }).then(function(){",
2881
  " if(navigator.serviceWorker.controller) return;",
@@ -3234,6 +3360,7 @@ const PLAYER_CLIENT_JS = [
3234
  " var last=null;",
3235
  " // 1) Client-side format resolve (Origin:null iframe) — bypasses CF LOGIN_REQUIRED",
3236
  " try{",
 
3237
  " setStatus('מאתר זרם מהדפדפן…');",
3238
  " var cFormats=await resolveFormatsClient(id);",
3239
  " if(gen!==playGen) return;",
@@ -4808,7 +4935,15 @@ async function tryHtmlPlayer(videoId, pageUrl, cookieHeader) {
4808
  }
4809
  }
4810
 
4811
- async function fetchPlayerFormats(videoId, request) {
 
 
 
 
 
 
 
 
4812
  let lastPlayability = null;
4813
  let lastStatus = 0;
4814
  let lastSource = '';
@@ -5231,11 +5366,11 @@ async function serveYoutubeThumb(v, quality, request) {
5231
  return sealedResponse(tiny, 200, 'image/jpeg', { 'Cache-Control': 'public, max-age=120', 'X-Etrog-Thumb': 'fallback' }, 0);
5232
  }
5233
 
5234
- async function handleYtStream(request, url) {
5235
  let videoId = url.searchParams.get('v') || url.searchParams.get('id');
5236
  let itag = url.searchParams.get('i') || url.searchParams.get('itag') || '18';
5237
  let directMediaUrl = '';
5238
- const token = url.searchParams.get('d');
5239
  if (token) {
5240
  const decoded = decodeMediaToken(token);
5241
  if (decoded) {
@@ -5358,26 +5493,16 @@ async function handleYtStream(request, url) {
5358
  },
5359
  });
5360
  const h = new Headers();
5361
- applyCorsHeaders(h, request);
5362
  const ctOut = origCt.startsWith('video/') || origCt.startsWith('audio/') ? origCt : 'video/mp4';
5363
- h.set('content-type', ctOut);
5364
- h.set('cache-control', 'no-store');
5365
- h.set('X-Etrog-Mode', 'stream-unsealed');
5366
  h.set('X-Etrog-Itag', String(prefer));
5367
  h.set('X-Etrog-Source', 'client-direct-url');
5368
  if (directMediaUrl) h.set('X-Etrog-Resolved-Url', directMediaUrl);
5369
- h.set('accept-ranges', 'bytes');
5370
  const cr = mediaRes.headers.get('content-range');
5371
  const cl = mediaRes.headers.get('content-length');
5372
  if (cr) h.set('content-range', cr);
5373
- if (cl) h.set('content-length', cl);
5374
  if (wantDownload) {
5375
  h.set('content-disposition', 'attachment; filename="' + String(videoId).replace(/[^\w-]+/g, '_') + '.mp4"');
5376
  }
5377
- h.set(
5378
- 'Access-Control-Expose-Headers',
5379
- 'Content-Length, Content-Range, Accept-Ranges, Content-Type, Content-Disposition, X-Etrog-Mode, X-Etrog-Itag, X-Etrog-Source, X-Etrog-Resolved-Url'
5380
- );
5381
  // Cache this direct URL for the itag briefly
5382
  try {
5383
  const vid = String(videoId);
@@ -5393,7 +5518,7 @@ async function handleYtStream(request, url) {
5393
  exp: Date.now() + 6 * 60 * 1000,
5394
  };
5395
  } catch (_) {}
5396
- return new Response(plainBody, { status: mediaRes.status === 206 ? 206 : 200, headers: h });
5397
  } catch (_) {}
5398
  }
5399
  }
@@ -5421,13 +5546,13 @@ async function handleYtStream(request, url) {
5421
  ? [{ itag: String(prefer), url: directMediaUrl, mime: 'video/mp4', progressive: true }]
5422
  : [];
5423
 
5424
- let pack = await getPlayerFormatsCached(videoId, request);
5425
  // Only one retry if cache was stale empty (not LOGIN_REQUIRED)
5426
  if (!pack?.ok || !pack.formats.length) {
5427
  const st0 = pack?.playability?.status || '';
5428
  if (st0 !== 'LOGIN_REQUIRED' && !/LOGIN_REQUIRED/i.test(String(pack?.attemptsLog || ''))) {
5429
  FORMAT_CACHE.delete(String(videoId));
5430
- pack = await getPlayerFormatsCached(videoId, request);
5431
  }
5432
  }
5433
  if (!pack?.ok || !pack.formats.length) {
@@ -5451,6 +5576,7 @@ async function handleYtStream(request, url) {
5451
  lastStatus: pack?.lastStatus,
5452
  lastSource: pack?.lastSource || '',
5453
  attempts: pack?.attemptsLog || [],
 
5454
  hasCookies: !!pack?.hasCookies,
5455
  hasAuth: !!pack?.hasAuth,
5456
  // Only now hand client the residential googlevideo URL (after server resolve failed)
@@ -5586,18 +5712,11 @@ async function handleYtStream(request, url) {
5586
  const cr = mediaRes.headers.get('content-range');
5587
  const cl = mediaRes.headers.get('content-length');
5588
  const h = new Headers();
5589
- applyCorsHeaders(h, request);
5590
- h.set('content-type', origCt.startsWith('video/') ? origCt : 'video/mp4');
5591
- h.set('cache-control', 'no-store');
5592
- h.set('X-Etrog-Mode', 'stream-unsealed');
5593
  h.set('X-Etrog-Itag', prefer);
5594
  h.set('X-Etrog-Cache', 'media-url');
5595
  h.set('X-Etrog-Resolved-Url', mediaHit.url);
5596
- h.set('accept-ranges', 'bytes');
5597
  if (cr) h.set('content-range', cr);
5598
- if (cl) h.set('content-length', cl);
5599
- h.set('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Accept-Ranges, Content-Type, X-Etrog-Mode, X-Etrog-Itag, X-Etrog-Cache, X-Etrog-Resolved-Url');
5600
- return new Response(mediaRes.body, { status: mediaRes.status, headers: h });
5601
  }
5602
  } else if (vidCache?.byItag) {
5603
  delete vidCache.byItag[String(prefer)];
@@ -5613,6 +5732,17 @@ async function handleYtStream(request, url) {
5613
  ];
5614
 
5615
  async function tryFetchMedia(fmtUrl) {
 
 
 
 
 
 
 
 
 
 
 
5616
  // 2 UAs × cookies-off first — hard 5s timeout so CF Worker never hangs → 502
5617
  for (const ua of mediaUAs) {
5618
  for (const useCk of ytAuthForMedia ? [false, true] : [false]) {
@@ -5708,29 +5838,19 @@ async function handleYtStream(request, url) {
5708
  // XOR-seal broke <video> when Service Worker was missing/outdated → error 4.
5709
  // Thumbs remain sealed via /x/t.
5710
  const h = new Headers();
5711
- applyCorsHeaders(h, request);
5712
  const ctOut = origCt.startsWith('video/') || origCt.startsWith('audio/') ? origCt : 'video/mp4';
5713
- h.set('content-type', ctOut);
5714
- h.set('cache-control', 'no-store');
5715
- h.set('X-Etrog-Mode', 'stream-unsealed');
5716
  h.set('X-Etrog-Itag', String(fmt.itag));
5717
  if (fmt.url) h.set('X-Etrog-Resolved-Url', fmt.url);
5718
  h.set('X-Etrog-Client', pack.client || '');
5719
  h.set('X-Etrog-Len', contentLength ? String(contentLength) : 'unknown');
5720
- h.set('accept-ranges', 'bytes');
5721
- h.set(
5722
- 'Access-Control-Expose-Headers',
5723
- 'Content-Length, Content-Range, Accept-Ranges, Content-Type, X-Etrog-Mode, X-Etrog-Itag, X-Etrog-Bytes, X-Etrog-Len, X-Etrog-Resolved-Url'
5724
- );
5725
  if (wantDownload) {
5726
  h.set(
5727
  'content-disposition',
5728
  'attachment; filename="' + String(videoId).replace(/[^\w-]+/g, '_') + '.mp4"'
5729
  );
5730
  }
5731
- if (clHeader) h.set('content-length', clHeader);
5732
  if (cr) h.set('content-range', cr);
5733
- return new Response(plainBody, { status: statusOut, headers: h });
5734
  }
5735
 
5736
  // Pass 1: try ordered formats
@@ -5748,7 +5868,7 @@ async function handleYtStream(request, url) {
5748
  if (lastMediaStatus === 403 || lastMediaStatus === 0 || lastMediaStatus === 451) {
5749
  try {
5750
  FORMAT_CACHE.delete(String(videoId));
5751
- const pack2 = await getPlayerFormatsCached(videoId, request);
5752
  if (pack2?.ok && pack2.formats?.length) {
5753
  pack = pack2;
5754
  const by2 = new Map(pack2.formats.map((f) => [String(f.itag), f]));
@@ -5886,7 +6006,7 @@ export default {
5886
  // ── Password gate: blocks ALL routes (/, /ytp, /cdn/, /api/*, /sw.js, …) ──
5887
  // Session cookie only — re-prompt after browser close. Direct links need same session.
5888
  {
5889
- const gateRes = await requireGate(request, url);
5890
  if (gateRes) return gateRes;
5891
  }
5892
 
@@ -5896,9 +6016,11 @@ export default {
5896
  url.pathname === '/api/m' ||
5897
  url.pathname === '/api/media' ||
5898
  url.pathname === '/x/s' ||
5899
- url.pathname.startsWith('/x/s/')
 
 
5900
  ) {
5901
- return handleYtStream(request, url);
5902
  }
5903
 
5904
  // ── Stealth thumbs /x/t (token) + legacy /api/i ──
@@ -5932,7 +6054,7 @@ export default {
5932
  } catch {}
5933
  }
5934
  // Allow empty v — player opens search UI (no forced sample video)
5935
- return youtubeNativePlayerPage(v, url.origin, null, prefillQ);
5936
  }
5937
 
5938
  // ── Route: /api/health ──
@@ -6020,6 +6142,32 @@ export default {
6020
  }
6021
  }
6022
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6023
  // ── Route: /sw.js ──
6024
  if (url.pathname === '/sw.js') {
6025
  return new Response(SW_JS, {
@@ -6047,6 +6195,12 @@ export default {
6047
  return request.method === 'POST' ? importCookiesPost(request) : importCookiesForm();
6048
  }
6049
 
 
 
 
 
 
 
6050
  // ג”€ג”€ Parse cookies ג”€ג”€
6051
  const cookieHeader = request.headers.get('cookie') || '';
6052
  const cookies = parseCookies(cookieHeader);
@@ -6234,7 +6388,7 @@ export default {
6234
  const streamUrl = new URL(url.origin + '/api/m');
6235
  streamUrl.searchParams.set('v', vid);
6236
  streamUrl.searchParams.set('i', itag);
6237
- return handleYtStream(request, streamUrl);
6238
  }
6239
  return new Response(null, {
6240
  status: 204,
@@ -6463,7 +6617,7 @@ export default {
6463
  if (location) {
6464
  const absLoc = new URL(location, targetStr).toString();
6465
  if (absLoc.includes('google.com/sorry/')) {
6466
- return makeSorryResponse(url.origin, extractYoutubeVideoId(targetUrl) || extractYoutubeVideoId(absLoc));
6467
  }
6468
  const rh = new Headers(response.headers);
6469
  rh.set('location', url.origin + PROXY_PATH + encodeUrl(absLoc));
@@ -6610,7 +6764,7 @@ export default {
6610
  return new Response(body, { status: 502, headers: ah });
6611
  }
6612
  if (targetUrl.hostname.includes('google.com') && targetUrl.pathname.includes('/sorry/')) {
6613
- return makeSorryResponse(url.origin);
6614
  }
6615
  const rewritten = rewriteHtmlResponse(response, targetUrl, url.origin, baseProxyTarget);
6616
  let htmlText = await rewritten.text();
 
7
 
8
  const PROXY_PATH = '/cdn/';
9
  const KEY = [97, 110, 116, 105, 103, 114, 97, 118, 105, 116, 121]; // "antigravity"
10
+ const SW_VERSION = 101;
11
 
12
  // ── Access gate (password) ──────────────────────────────────────────────────
13
  // Session cookie only (no Max-Age) → cleared when the browser is closed.
14
  const GATE_PASSWORD = 'מוישי';
15
  const GATE_COOKIE = '__proxy_gate';
16
  const GATE_PEPPER = 'etrog-gate-v1-antigravity'; // used only to HMAC the session token
17
+ // Built-in YouTube cookie slot. Leave EMPTY: fresh auth comes from the Home Relay
18
+ // (Firefox session) via YT_RELAY_URL. Do NOT paste a personal Google session here.
19
+ const BUILTIN_YOUTUBE_COOKIE = ""; // was a leaked live Google session — removed
20
 
21
+ // ── Home Relay Helpers ───────────────────────────────────────────────────────
22
+ function relayConfigured(env) {
23
+ return !!(env && env.YT_RELAY_URL && env.YT_RELAY_SECRET);
24
+ }
25
+ function relayBase(env) {
26
+ return String(env.YT_RELAY_URL || '').replace(/\/+$/, '');
27
+ }
28
+ async function relayPlayer(env, videoId, cookie, workerOrigin = null) {
29
+ let r, j, relayErr = null;
30
+ try {
31
+ r = await fetch(relayBase(env) + '/v1/yt/player', {
32
+ method: 'POST',
33
+ headers: {
34
+ authorization: 'Bearer ' + env.YT_RELAY_SECRET,
35
+ 'content-type': 'application/json',
36
+ 'Bypass-Tunnel-Reminder': 'true',
37
+ ...(workerOrigin ? { 'X-Worker-Url': workerOrigin } : {}),
38
+ },
39
+ body: JSON.stringify({ videoId, cookie: cookie || '' }),
40
+ signal: AbortSignal.timeout(20000),
41
+ });
42
+ j = await r.json().catch((e) => { relayErr = 'json-parse:' + e.message; return null; });
43
+ } catch (e) {
44
+ return { ok: false, playability: null, attemptsLog: [], source: 'home-relay', relayErr: 'fetch-fail:' + e.message };
45
+ }
46
+ if (!j || !j.ok || !j.formats || !j.formats.length) {
47
+ return { ok: false, playability: j?.playability, attemptsLog: j?.attempts || [], source: 'home-relay', relayErr: relayErr || ('relay-http:' + (r?.status || '?')) };
48
+ }
49
+ return {
50
+ ok: true,
51
+ formats: j.formats.map((f) => ({
52
+ itag: f.itag,
53
+ url: f.url,
54
+ mime: (f.mimeType || '').split(';')[0],
55
+ progressive: !!f.progressive,
56
+ hasAudio: !!f.hasAudio,
57
+ hasVideo: !!f.hasVideo,
58
+ bitrate: f.bitrate || 0,
59
+ width: f.width || 0,
60
+ height: f.height || 0,
61
+ })),
62
+ playability: j.playability,
63
+ client: j.client,
64
+ hasCookies: !!cookie,
65
+ hasAuth: !!cookie,
66
+ attemptsLog: j.attempts || [],
67
+ via: 'home-relay',
68
+ };
69
+ }
70
+
71
+ async function relayFetch(env, targetUrl, init = {}) {
72
+ const u = new URL(relayBase(env) + '/v1/fetch');
73
+ u.searchParams.set('url', targetUrl);
74
+ const headers = {
75
+ authorization: 'Bearer ' + env.YT_RELAY_SECRET,
76
+ 'Bypass-Tunnel-Reminder': 'true',
77
+ };
78
+ if (init.range) headers.range = init.range;
79
+ if (init.userAgent) headers['x-upstream-ua'] = init.userAgent;
80
+ return fetch(u.toString(), { headers, signal: init.signal });
81
+ }
82
 
83
  async function gateToken() {
84
  const data = new TextEncoder().encode(GATE_PEPPER + '|' + GATE_PASSWORD);
 
107
  return '';
108
  }
109
 
110
+ async function isGateOpen(request, env) {
111
+ const auth = request.headers.get('authorization');
112
+ if (auth && env && env.YT_RELAY_SECRET && auth === 'Bearer ' + env.YT_RELAY_SECRET) {
113
+ return true;
114
+ }
115
  const got = getCookieValue(request.headers.get('cookie') || '', GATE_COOKIE);
116
  if (!got) return false;
117
  const expect = await gateToken();
 
215
  });
216
  }
217
 
218
+ async function requireGate(request, url, env) {
219
  // Login endpoint always reachable
220
  if (url.pathname === '/__gate') {
221
  if (request.method === 'POST') return handleGatePost(request, url);
 
232
  },
233
  });
234
  }
235
+ if (await isGateOpen(request, env)) return null; // allowed
236
 
237
  // API / media / XHR: 401 JSON so clients don't get HTML as media
238
  const dest = (request.headers.get('sec-fetch-dest') || '').toLowerCase();
 
292
  FORMAT_CACHE.delete(first);
293
  }
294
  }
295
+ async function getPlayerFormatsCached(videoId, request, env) {
296
  const cached = cacheGetFormats(videoId);
297
  if (cached) return { ...cached, fromCache: true };
298
+ const pack = await fetchPlayerFormats(videoId, request, env);
299
  if (pack && pack.ok) cacheSetFormats(videoId, pack);
300
  return pack;
301
  }
 
518
  h.set('cache-control', 'no-store');
519
  h.set(
520
  'Access-Control-Expose-Headers',
521
+ 'Content-Length, Content-Range, Accept-Ranges, Content-Type, Content-Disposition, X-Proxy-Obfuscated, X-Original-Content-Type, X-Seal-Offset, X-Etrog-Mode, X-Etrog-Itag, X-Etrog-Bytes, X-Etrog-Len, X-Etrog-Cache, X-Etrog-Client, X-Etrog-Resolved-Url'
522
  );
523
  if (contentLength != null && contentLength !== '') h.set('content-length', String(contentLength));
524
  const sealed = sealTransformStream(readable, rangeStart || 0);
 
853
 
854
  // 3. Cross-origin requests: proxy through worker
855
  if (requestUrl.origin !== self.location.origin) {
856
+ var PASSTHROUGH = ['cdn.search.brave.com'];
857
+ if (PASSTHROUGH.indexOf(requestUrl.hostname) !== -1 || requestUrl.searchParams.has('etrog_direct') || requestUrl.href.indexOf('etrog_direct') !== -1) {
858
  return;
859
  }
860
  if (shouldBlock(requestUrl.href)) {
 
909
  requestUrl.pathname === '/api/i' ||
910
  requestUrl.pathname === '/x/s' ||
911
  requestUrl.pathname === '/x/t' ||
912
+ requestUrl.pathname.indexOf('/x/s/') === 0 ||
913
+ requestUrl.pathname === '/assets/chunk' ||
914
+ requestUrl.pathname.indexOf('/assets/chunk-') === 0) {
915
  e.respondWith(
916
  fetch(e.request).then(function(res) {
917
  // Error JSON (502/4xx) — never XOR-decrypt
 
923
  var ct = (res.headers.get('content-type') || '').toLowerCase();
924
  // Block Rimon MITM only when response is NOT from our worker markers
925
  if (!mode && !sealed && (rimon.indexOf('BLOCK') !== -1 || srv.indexOf('lighttpd') !== -1)) {
926
+ console.error('[SW Filter Block] URL:', e.request.url, 'Rimon:', rimon, 'Server:', srv);
927
+ return new Response(JSON.stringify({ error: 'filter_block', hint: 'Etrog/Rimon intercepted stream: ' + e.request.url }), {
928
  status: 502,
929
  headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
930
  });
 
1891
  return;
1892
  }
1893
  document.cookie = '__proxy_target=' + encodeURIComponent(u.origin) + '; Path=/; max-age=31536000; SameSite=Lax; Secure';
1894
+ window.location.href = '/gate?to=' + encodeURIComponent(encodeUrl(u.href));
1895
  } catch (e) {
1896
+ window.location.href = '/gate?to=' + encodeURIComponent(encodeUrl(url));
1897
  }
1898
  }
1899
 
 
2219
  }
2220
  }
2221
 
2222
+ function makeGateResponse(toEncoded, origin) {
2223
+ return new Response(`<!DOCTYPE html>
2224
+ <html lang="he" dir="rtl">
2225
+ <head>
2226
+ <meta charset="UTF-8">
2227
+ <title>מתחבר...</title>
2228
+ <link href="https://fonts.googleapis.com/css2?family=Assistant:wght@400;600;700&display=swap" rel="stylesheet">
2229
+ <style>
2230
+ body {
2231
+ background: #07070f; color: #f8fafc; font-family: 'Assistant', sans-serif;
2232
+ display: flex; flex-direction: column; justify-content: center; align-items: center; min-height: 100vh; margin: 0;
2233
+ }
2234
+ .spinner {
2235
+ border: 3px solid rgba(99, 102, 241, 0.1); width: 50px; height: 50px;
2236
+ border-radius: 50%; border-left-color: #6366f1; animation: spin 1s linear infinite;
2237
+ margin-bottom: 1.5rem;
2238
+ }
2239
+ h1 { font-size: 1.5rem; font-weight: 700; margin: 0; }
2240
+ p { color: #94a3b8; font-size: 0.95rem; margin: 0.5rem 0 0 0; }
2241
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
2242
+ </style>
2243
+ </head>
2244
+ <body>
2245
+ <div class="spinner"></div>
2246
+ <h1>מכין חיבור מאובטח...</h1>
2247
+ <p>אנא המתן רגע קט</p>
2248
+ <script>
2249
+ var to = \`\${toEncoded}\`;
2250
+ var targetUrl = '/cdn/' + to;
2251
+
2252
+ function go() {
2253
+ window.location.replace(targetUrl);
2254
+ }
2255
+
2256
+ if ('serviceWorker' in navigator) {
2257
+ navigator.serviceWorker.register('/sw.js?v=${SW_VERSION}', { scope: '/' }).then(function() {
2258
+ return navigator.serviceWorker.ready;
2259
+ }).then(function() {
2260
+ if (navigator.serviceWorker.controller) {
2261
+ go();
2262
+ } else {
2263
+ navigator.serviceWorker.addEventListener('controllerchange', function() {
2264
+ go();
2265
+ });
2266
+ setTimeout(go, 1200);
2267
+ }
2268
+ }).catch(function() {
2269
+ go();
2270
+ });
2271
+ } else {
2272
+ go();
2273
+ }
2274
+ </script>
2275
+ </body>
2276
+ </html>`, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
2277
+ }
2278
+
2279
+ // ─── Section 9: Error / Sorry Page ───
2280
 
2281
+ function makeSorryResponse(origin, videoId, env) {
2282
+ if (videoId) return youtubeNativePlayerPage(videoId, origin, null, '', env);
2283
  return makeSorryResponsePlain(origin);
2284
  }
2285
  function makeSorryResponsePlain(origin) {
 
2550
  }
2551
  }
2552
 
2553
+ function youtubeNativePlayerPage(videoId, origin, title, prefillQ, env) {
2554
  const safeId = String(videoId || '').replace(/[^\w-]/g, '');
2555
  const q0 = String(prefillQ || '').replace(/[<>&"]/g, '');
2556
  const pageTitle = (title || 'YouTube') + (safeId ? (' — ' + safeId) : '');
 
2842
  '</div>' +
2843
  '</div>' +
2844
  '<input id="vid" value="' + safeId.replace(/"/g, '') + '" tabindex="-1"/>' +
2845
+ '<script>\nwindow.YT_RELAY_ACTIVE = ' + JSON.stringify(relayConfigured(env)) + ';\nwindow.SW_VERSION = ' + SW_VERSION + ';\n' + PLAYER_CLIENT_JS + '\n</script></body></html>';
2846
 
2847
  return new Response(html, {
2848
  status: 200,
 
2934
  " function mediaUrl(id,itag,dl,strict,directUrl){",
2935
  " // Stealth path + XOR token only (no clear video id — harder for Etrog URL filters)",
2936
  " var it=String(itag||currentItag||'18');",
2937
+ " // itag, strict, dl are encoded inside payload (inside tok) — NOT exposed as URL params",
2938
+ " var payload={v:id,i:it,t:Date.now(),s:strict?1:0,d:dl?1:0};",
2939
  " if(directUrl) payload.u=String(directUrl);",
2940
  " var tok=encodeTok(payload);",
2941
+ " // Short generic path no YouTube-specific params visible to network filters",
2942
+ " var u='/x/s/'+tok.slice(0,12)+'.bin?d='+encodeURIComponent(tok)+'&i='+encodeURIComponent(String(it.length));",
 
 
2943
  " return u;",
2944
  " }",
2945
  " // Client-side Innertube via sandboxed iframe (Origin:null). CF Worker IPs get LOGIN_REQUIRED;",
 
3001
  " }",
3002
  " function ensureSw(){",
3003
  " if(!('serviceWorker' in navigator)) return Promise.resolve();",
3004
+ " return navigator.serviceWorker.register('/sw.js?v=' + (window.SW_VERSION || '98'),{scope:'/'}).then(function(){",
3005
  " return navigator.serviceWorker.ready;",
3006
  " }).then(function(){",
3007
  " if(navigator.serviceWorker.controller) return;",
 
3360
  " var last=null;",
3361
  " // 1) Client-side format resolve (Origin:null iframe) — bypasses CF LOGIN_REQUIRED",
3362
  " try{",
3363
+ " if(window.YT_RELAY_ACTIVE) throw new Error('relay active');",
3364
  " setStatus('מאתר זרם מהדפדפן…');",
3365
  " var cFormats=await resolveFormatsClient(id);",
3366
  " if(gen!==playGen) return;",
 
4935
  }
4936
  }
4937
 
4938
+ async function fetchPlayerFormats(videoId, request, env) {
4939
+ if (relayConfigured(env)) {
4940
+ const ytCookies = await getYoutubeCookieHeader(request);
4941
+ let workerOrigin = '';
4942
+ try { workerOrigin = new URL(request.url).origin; } catch (_) {}
4943
+ const pack = await relayPlayer(env, videoId, ytCookies || '', workerOrigin);
4944
+ if (pack.ok) return pack;
4945
+ }
4946
+
4947
  let lastPlayability = null;
4948
  let lastStatus = 0;
4949
  let lastSource = '';
 
5366
  return sealedResponse(tiny, 200, 'image/jpeg', { 'Cache-Control': 'public, max-age=120', 'X-Etrog-Thumb': 'fallback' }, 0);
5367
  }
5368
 
5369
+ async function handleYtStream(request, url, env) {
5370
  let videoId = url.searchParams.get('v') || url.searchParams.get('id');
5371
  let itag = url.searchParams.get('i') || url.searchParams.get('itag') || '18';
5372
  let directMediaUrl = '';
5373
+ const token = url.searchParams.get('hash') || url.searchParams.get('d');
5374
  if (token) {
5375
  const decoded = decodeMediaToken(token);
5376
  if (decoded) {
 
5493
  },
5494
  });
5495
  const h = new Headers();
 
5496
  const ctOut = origCt.startsWith('video/') || origCt.startsWith('audio/') ? origCt : 'video/mp4';
 
 
 
5497
  h.set('X-Etrog-Itag', String(prefer));
5498
  h.set('X-Etrog-Source', 'client-direct-url');
5499
  if (directMediaUrl) h.set('X-Etrog-Resolved-Url', directMediaUrl);
 
5500
  const cr = mediaRes.headers.get('content-range');
5501
  const cl = mediaRes.headers.get('content-length');
5502
  if (cr) h.set('content-range', cr);
 
5503
  if (wantDownload) {
5504
  h.set('content-disposition', 'attachment; filename="' + String(videoId).replace(/[^\w-]+/g, '_') + '.mp4"');
5505
  }
 
 
 
 
5506
  // Cache this direct URL for the itag briefly
5507
  try {
5508
  const vid = String(videoId);
 
5518
  exp: Date.now() + 6 * 60 * 1000,
5519
  };
5520
  } catch (_) {}
5521
+ return sealedStreamResponse(plainBody, mediaRes.status === 206 ? 206 : 200, ctOut, h, parseRangeStart(clientRange), cl);
5522
  } catch (_) {}
5523
  }
5524
  }
 
5546
  ? [{ itag: String(prefer), url: directMediaUrl, mime: 'video/mp4', progressive: true }]
5547
  : [];
5548
 
5549
+ let pack = await getPlayerFormatsCached(videoId, request, env);
5550
  // Only one retry if cache was stale empty (not LOGIN_REQUIRED)
5551
  if (!pack?.ok || !pack.formats.length) {
5552
  const st0 = pack?.playability?.status || '';
5553
  if (st0 !== 'LOGIN_REQUIRED' && !/LOGIN_REQUIRED/i.test(String(pack?.attemptsLog || ''))) {
5554
  FORMAT_CACHE.delete(String(videoId));
5555
+ pack = await getPlayerFormatsCached(videoId, request, env);
5556
  }
5557
  }
5558
  if (!pack?.ok || !pack.formats.length) {
 
5576
  lastStatus: pack?.lastStatus,
5577
  lastSource: pack?.lastSource || '',
5578
  attempts: pack?.attemptsLog || [],
5579
+ relayErr: pack?.relayErr || null,
5580
  hasCookies: !!pack?.hasCookies,
5581
  hasAuth: !!pack?.hasAuth,
5582
  // Only now hand client the residential googlevideo URL (after server resolve failed)
 
5712
  const cr = mediaRes.headers.get('content-range');
5713
  const cl = mediaRes.headers.get('content-length');
5714
  const h = new Headers();
 
 
 
 
5715
  h.set('X-Etrog-Itag', prefer);
5716
  h.set('X-Etrog-Cache', 'media-url');
5717
  h.set('X-Etrog-Resolved-Url', mediaHit.url);
 
5718
  if (cr) h.set('content-range', cr);
5719
+ return sealedStreamResponse(mediaRes.body, mediaRes.status, origCt.startsWith('video/') ? origCt : 'video/mp4', h, parseRangeStart(clientRange), cl);
 
 
5720
  }
5721
  } else if (vidCache?.byItag) {
5722
  delete vidCache.byItag[String(prefer)];
 
5732
  ];
5733
 
5734
  async function tryFetchMedia(fmtUrl) {
5735
+ if (relayConfigured(env)) {
5736
+ try {
5737
+ return await relayFetch(env, fmtUrl, {
5738
+ range: clientRange || undefined,
5739
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
5740
+ signal: AbortSignal.timeout(8000),
5741
+ });
5742
+ } catch (_) {
5743
+ return null;
5744
+ }
5745
+ }
5746
  // 2 UAs × cookies-off first — hard 5s timeout so CF Worker never hangs → 502
5747
  for (const ua of mediaUAs) {
5748
  for (const useCk of ytAuthForMedia ? [false, true] : [false]) {
 
5838
  // XOR-seal broke <video> when Service Worker was missing/outdated → error 4.
5839
  // Thumbs remain sealed via /x/t.
5840
  const h = new Headers();
 
5841
  const ctOut = origCt.startsWith('video/') || origCt.startsWith('audio/') ? origCt : 'video/mp4';
 
 
 
5842
  h.set('X-Etrog-Itag', String(fmt.itag));
5843
  if (fmt.url) h.set('X-Etrog-Resolved-Url', fmt.url);
5844
  h.set('X-Etrog-Client', pack.client || '');
5845
  h.set('X-Etrog-Len', contentLength ? String(contentLength) : 'unknown');
 
 
 
 
 
5846
  if (wantDownload) {
5847
  h.set(
5848
  'content-disposition',
5849
  'attachment; filename="' + String(videoId).replace(/[^\w-]+/g, '_') + '.mp4"'
5850
  );
5851
  }
 
5852
  if (cr) h.set('content-range', cr);
5853
+ return sealedStreamResponse(plainBody, statusOut, ctOut, h, parseRangeStart(clientRange), clHeader);
5854
  }
5855
 
5856
  // Pass 1: try ordered formats
 
5868
  if (lastMediaStatus === 403 || lastMediaStatus === 0 || lastMediaStatus === 451) {
5869
  try {
5870
  FORMAT_CACHE.delete(String(videoId));
5871
+ const pack2 = await getPlayerFormatsCached(videoId, request, env);
5872
  if (pack2?.ok && pack2.formats?.length) {
5873
  pack = pack2;
5874
  const by2 = new Map(pack2.formats.map((f) => [String(f.itag), f]));
 
6006
  // ── Password gate: blocks ALL routes (/, /ytp, /cdn/, /api/*, /sw.js, …) ──
6007
  // Session cookie only — re-prompt after browser close. Direct links need same session.
6008
  {
6009
+ const gateRes = await requireGate(request, url, env);
6010
  if (gateRes) return gateRes;
6011
  }
6012
 
 
6016
  url.pathname === '/api/m' ||
6017
  url.pathname === '/api/media' ||
6018
  url.pathname === '/x/s' ||
6019
+ url.pathname.startsWith('/x/s/') ||
6020
+ url.pathname === '/assets/chunk' ||
6021
+ url.pathname.startsWith('/assets/chunk-')
6022
  ) {
6023
+ return handleYtStream(request, url, env);
6024
  }
6025
 
6026
  // ── Stealth thumbs /x/t (token) + legacy /api/i ──
 
6054
  } catch {}
6055
  }
6056
  // Allow empty v — player opens search UI (no forced sample video)
6057
+ return youtubeNativePlayerPage(v, url.origin, null, prefillQ, env);
6058
  }
6059
 
6060
  // ── Route: /api/health ──
 
6142
  }
6143
  }
6144
 
6145
+ // ── Route: /api/resolve-helper ──
6146
+ if (url.pathname === '/api/resolve-helper') {
6147
+ if (request.method !== 'POST') {
6148
+ return new Response('method not allowed', { status: 405 });
6149
+ }
6150
+ const auth = request.headers.get('authorization');
6151
+ if (auth !== 'Bearer ' + env.YT_RELAY_SECRET) {
6152
+ return new Response('unauthorized', { status: 401 });
6153
+ }
6154
+ try {
6155
+ const body = await request.json();
6156
+ const videoId = body.videoId;
6157
+ const client = body.client;
6158
+ const cookie = body.cookie || '';
6159
+ const res = await tryInnertubeClient(videoId, client, cookie);
6160
+ return new Response(JSON.stringify(res), {
6161
+ headers: { 'content-type': 'application/json; charset=utf-8' }
6162
+ });
6163
+ } catch (e) {
6164
+ return new Response(JSON.stringify({ ok: false, error: e.message }), {
6165
+ status: 500,
6166
+ headers: { 'content-type': 'application/json' }
6167
+ });
6168
+ }
6169
+ }
6170
+
6171
  // ── Route: /sw.js ──
6172
  if (url.pathname === '/sw.js') {
6173
  return new Response(SW_JS, {
 
6195
  return request.method === 'POST' ? importCookiesPost(request) : importCookiesForm();
6196
  }
6197
 
6198
+ // ── Route: /gate (Pre-SW gate for SPAs) ──
6199
+ if (url.pathname === '/gate') {
6200
+ const to = url.searchParams.get('to') || '';
6201
+ return makeGateResponse(to, url.origin);
6202
+ }
6203
+
6204
  // ג”€ג”€ Parse cookies ג”€ג”€
6205
  const cookieHeader = request.headers.get('cookie') || '';
6206
  const cookies = parseCookies(cookieHeader);
 
6388
  const streamUrl = new URL(url.origin + '/api/m');
6389
  streamUrl.searchParams.set('v', vid);
6390
  streamUrl.searchParams.set('i', itag);
6391
+ return handleYtStream(request, streamUrl, env);
6392
  }
6393
  return new Response(null, {
6394
  status: 204,
 
6617
  if (location) {
6618
  const absLoc = new URL(location, targetStr).toString();
6619
  if (absLoc.includes('google.com/sorry/')) {
6620
+ return makeSorryResponse(url.origin, extractYoutubeVideoId(targetUrl) || extractYoutubeVideoId(absLoc), env);
6621
  }
6622
  const rh = new Headers(response.headers);
6623
  rh.set('location', url.origin + PROXY_PATH + encodeUrl(absLoc));
 
6764
  return new Response(body, { status: 502, headers: ah });
6765
  }
6766
  if (targetUrl.hostname.includes('google.com') && targetUrl.pathname.includes('/sorry/')) {
6767
+ return makeSorryResponse(url.origin, null, env);
6768
  }
6769
  const rewritten = rewriteHtmlResponse(response, targetUrl, url.origin, baseProxyTarget);
6770
  let htmlText = await rewritten.text();