// ==UserScript== // @name Replace activitypods & proxy OIDC path // @namespace http://tampermonkey.net/ // @version 1.2 // @description Intercept fetch/XHR to activitypods pod-providers and proxy pds-sp.whey.party OIDC discovery path to .oidc/auth/... path // @author You // @match *://*/* // @grant none // @run-at document-start // ==/UserScript== (() => { 'use strict'; // ======= EDIT THIS: your replacement JSON object ======= const REPLACEMENT_JSON = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://activitypods.org/.well-known/context.json" ], "id": "https://activitypods.org/data/pod-providers", "type": [ "ldp:Container", "ldp:BasicContainer" ], "ldp:contains": [ { "id": "https://activitypods.org/data/pod-providers/armoise.co", "type": "apods:PodProvider", "apods:baseUrl": "https://armoise.co", "apods:area": "Oise, France", "apods:locales": "fr", "apods:providedBy": "Réseaux de Vie" }, { "id": "https://activitypods.org/data/pod-providers/mypod.store", "type": "apods:PodProvider", "apods:baseUrl": "https://mypod.store", "apods:area": "France", "apods:locales": "en", "apods:providedBy": "Assemblée Virtuelle" }, { "id": "https://activitypods.org/data/pod-providers/pds-sp.whey.party", "type": "apods:PodProvider", "apods:baseUrl": "https://pds-sp.whey.party", "apods:area": "TheWeb", "apods:locales": "en", "apods:providedBy": "Wowwww" } ] }; // ====================================================== // ActivityPods target const AP_ORIGIN = 'https://activitypods.org'; const AP_PATH = '/data/pod-providers'; const AP_FULL = AP_ORIGIN + AP_PATH; // OIDC proxy: source -> forward to target const OIDC_SOURCE = 'https://pds-sp.whey.party/.well-known/openid-configuration'; const OIDC_SOURCE2 = 'https://pds-sp.whey.party/whey/.well-known/openid-configuration' const OIDC_TARGET = 'https://pds-sp.whey.party/.oidc/auth/.well-known/openid-configuration'; function urlMatches(targetUrl, origin, path) { try { const parsed = new URL(targetUrl, location.origin); return parsed.origin === origin && parsed.pathname === path; } catch (e) { return false; } } function isExactUrl(url, exact) { try { const parsed = new URL(url, location.origin); return parsed.href === new URL(exact).href; } catch (e) { return false; } } // Helper to create a Response object for fetch from an object function makeResponseFromObject(obj) { const body = JSON.stringify(obj); return new Response(body, { status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/json; charset=utf-8', 'X-Userscript-Replaced': '1' } }); } // ---- Patch fetch ---- const originalFetch = window.fetch.bind(window); window.fetch = async function (input, init) { try { // Determine URL string from Request or string let reqUrl = input; if (input && typeof input === 'object' && input.url) { reqUrl = input.url; } // If it's the ActivityPods providers URL -> return replacement JSON if (isExactUrl(reqUrl, AP_FULL)) { return makeResponseFromObject(REPLACEMENT_JSON); } // If it's the OIDC source URL -> proxy to OIDC target if (isExactUrl(reqUrl, OIDC_SOURCE) || isExactUrl(reqUrl, OIDC_SOURCE2)) { // forward the original init where possible return originalFetch(OIDC_TARGET, init); } } catch (e) { console.error('userscript fetch interception error', e); // fallthrough to original fetch } return originalFetch(input, init); }; // Also patch globalThis.fetch if different try { if (globalThis.fetch !== window.fetch) { globalThis.fetch = window.fetch; } } catch (e) {} // ---- Patch XMLHttpRequest ---- const OriginalXHR = window.XMLHttpRequest; function fakeXHRResponse(xhr, replacementText, status = 200, statusText = 'OK', headers = {}) { // Populate common XHR fields and then call handlers / dispatch events asynchronously try { xhr.readyState = 4; } catch (e) {} try { xhr.status = status; } catch (e) {} try { xhr.statusText = statusText; } catch (e) {} try { // XHR often expects responseText and response Object.defineProperty(xhr, 'responseText', { value: replacementText, writable: false }); } catch (e) { try { xhr.responseText = replacementText; } catch (err) {} } try { Object.defineProperty(xhr, 'response', { value: replacementText, writable: false }); } catch (e) { try { xhr.response = replacementText; } catch (err) {} } // Optionally you could expose headers via getAllResponseHeaders, but that's more invasive. setTimeout(() => { try { if (typeof xhr.onreadystatechange === 'function') { xhr.onreadystatechange(); } if (typeof xhr.onload === 'function') { xhr.onload(); } // dispatch events so addEventListener handlers run try { xhr.dispatchEvent && xhr.dispatchEvent(new Event('readystatechange')); } catch (e) {} try { xhr.dispatchEvent && xhr.dispatchEvent(new Event('load')); } catch (e) {} } catch (err) { console.error('userscript XHR callback error', err); } }, 0); } function PatchedXHR() { const xhr = new OriginalXHR(); // Keep references to original methods const originalOpen = xhr.open; const originalSend = xhr.send; let _method = null; let _url = null; let _isAP = false; let _isOidcSource = false; xhr.open = function (method, url, async = true, user, password) { _method = method; _url = url; try { _isAP = isExactUrl(url, AP_FULL); _isOidcSource = isExactUrl(url, OIDC_SOURCE) || isExactUrl(url, OIDC_SOURCE2); } catch (e) { _isAP = false; _isOidcSource = false; } return originalOpen.apply(xhr, arguments); }; xhr.send = function (body) { // If it's the ActivityPods providers URL -> return replacement JSON (simulate response) if (_isAP) { try { const replacementText = JSON.stringify(REPLACEMENT_JSON); fakeXHRResponse(xhr, replacementText, 200, 'OK'); return; } catch (e) { console.error('userscript failed to simulate XHR response for AP', e); // fallthrough to originalSend } } // If it's the OIDC source URL -> fetch the OIDC target and simulate response if (_isOidcSource) { (async () => { try { const resp = await originalFetch(OIDC_TARGET, { method: _method || 'GET', // don't forward XHR body here; XHR discovery is GET normally // but preserve credentials mode if possible by not specifying mode/credentials }); const text = await resp.text(); fakeXHRResponse(xhr, text, resp.status || 200, resp.statusText || 'OK'); } catch (err) { console.error('userscript OIDC proxy fetch failed', err); // simulate an error XHR (status 0) so page sees a failure fakeXHRResponse(xhr, '', 0, 'Error'); } })(); return; } // otherwise fall back to normal send return originalSend.apply(xhr, arguments); }; return xhr; } // Keep prototype linkage to appear like a normal XHR PatchedXHR.prototype = OriginalXHR.prototype; window.XMLHttpRequest = PatchedXHR; console.info('Userscript active: AP replacement and OIDC proxy enabled.'); console.info('AP target:', AP_FULL); console.info('OIDC source -> target:', OIDC_SOURCE, '->', OIDC_TARGET); console.info('Current replacement JSON:', REPLACEMENT_JSON); })();