An in browser local PDS localpds.at
20

Configure Feed

Select the types of activity you want to include in your feed.

initial commit

authored by

Niall Bunting and committed by
Niall Bunting
(May 29, 2026, 9:00 AM +0100) fc48d301

+720
+661
background.js
··· 1 + const DOMAIN = "ds-pod.com"; 2 + 3 + // Keep track of which tab is currently attached to avoid global banner creep 4 + let attachedTabId = null; 5 + 6 + // Helper to safely handle base64 encoding with Unicode strings 7 + function safeBtoa(str) { 8 + return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => { 9 + return String.fromCharCode(parseInt(p1, 16)); 10 + })); 11 + } 12 + 13 + // Cryptographic JWT Generator for local mock sessions 14 + //async function nativeHS256(payload, secret) { 15 + // const header = { alg: "HS256", typ: "JWT" }; 16 + // 17 + // const b64 = (obj) => btoa(JSON.stringify(obj)) 18 + // .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); 19 + // 20 + // const encodedHeader = b64(header); 21 + // const encodedPayload = b64(payload); 22 + // const dataToSign = `${encodedHeader}.${encodedPayload}`; 23 + // 24 + // const key = await crypto.subtle.importKey( 25 + // "raw", 26 + // new TextEncoder().encode(secret), 27 + // { name: "HMAC", hash: "SHA-256" }, 28 + // false, 29 + // ["sign"] 30 + // ); 31 + // 32 + // const signature = await crypto.subtle.sign( 33 + // "HMAC", 34 + // key, 35 + // new TextEncoder().encode(dataToSign) 36 + // ); 37 + // 38 + // const encodedSignature = btoa(String.fromCharCode(...new Uint8Array(signature))) 39 + // .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); 40 + // 41 + // return `${dataToSign}.${encodedSignature}`; 42 + //} 43 + 44 + 45 + const STORAGE_KEY = 'atproto_keypair'; 46 + 47 + // Generate or load the persistent keypair 48 + async function getOrCreateKeypair() { 49 + const stored = await chrome.storage.local.get(STORAGE_KEY); 50 + 51 + if (stored[STORAGE_KEY]) { 52 + // Re-import stored keys 53 + const { privateKeyJwk, publicKeyJwk } = stored[STORAGE_KEY]; 54 + const privateKey = await crypto.subtle.importKey( 55 + 'jwk', privateKeyJwk, 56 + { name: 'ECDSA', namedCurve: 'P-256' }, 57 + false, ['sign'] 58 + ); 59 + const publicKey = await crypto.subtle.importKey( 60 + 'jwk', publicKeyJwk, 61 + { name: 'ECDSA', namedCurve: 'P-256' }, 62 + true, ['verify'] 63 + ); 64 + return { privateKey, publicKey, publicKeyJwk }; 65 + } 66 + 67 + // Generate fresh keypair 68 + const keypair = await crypto.subtle.generateKey( 69 + { name: 'ECDSA', namedCurve: 'P-256' }, 70 + true, ['sign', 'verify'] 71 + ); 72 + 73 + const privateKeyJwk = await crypto.subtle.exportKey('jwk', keypair.privateKey); 74 + const publicKeyJwk = await crypto.subtle.exportKey('jwk', keypair.publicKey); 75 + 76 + await chrome.storage.local.set({ 77 + [STORAGE_KEY]: { privateKeyJwk, publicKeyJwk } 78 + }); 79 + 80 + return { privateKey: keypair.privateKey, publicKey: keypair.publicKey, publicKeyJwk }; 81 + } 82 + 83 + const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; 84 + const BASE32_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; 85 + 86 + function toBase(bytes, alphabet) { 87 + let num = BigInt(0); 88 + for (const byte of bytes) { 89 + num = num * BigInt(256) + BigInt(byte); 90 + } 91 + 92 + let encoded = ''; 93 + while (num > BigInt(0)) { 94 + const remainder = num % BigInt(alphabet.length); 95 + num = num / BigInt(alphabet.length); 96 + encoded = alphabet[Number(remainder)] + encoded; 97 + } 98 + 99 + // Preserve leading zero bytes as '1' characters 100 + for (const byte of bytes) { 101 + if (byte === 0) encoded = '1' + encoded; 102 + else break; 103 + } 104 + 105 + return encoded; 106 + } 107 + 108 + // Encode public key as a did:key identifier 109 + // did:key uses multibase-encoded multicodec-prefixed public key bytes 110 + async function publicKeyJwkToDidKey(publicKeyJwk) { 111 + // Export raw public key bytes 112 + const publicKey = await crypto.subtle.importKey( 113 + 'jwk', publicKeyJwk, 114 + { name: 'ECDSA', namedCurve: 'P-256' }, 115 + true, ['verify'] 116 + ); 117 + 118 + const rawBytes = await crypto.subtle.exportKey('raw', publicKey); 119 + const pubKeyBytes = new Uint8Array(rawBytes); 120 + 121 + // Compress the public key (P-256 uncompressed is 65 bytes: 0x04 + x + y) 122 + // Compressed is 33 bytes: 0x02/0x03 + x 123 + const x = pubKeyBytes.slice(1, 33); 124 + const y = pubKeyBytes.slice(33, 65); 125 + const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03; 126 + const compressed = new Uint8Array([prefix, ...x]); 127 + 128 + // Multicodec prefix for P-256: 0x1200 (as varint) 129 + const multicodecPrefix = new Uint8Array([0x80, 0x24]); 130 + const prefixed = new Uint8Array([...multicodecPrefix, ...compressed]); 131 + 132 + // Multibase base58btc encode (prefix 'z') 133 + const base58 = toBase(prefixed, BASE32_ALPHABET); 134 + return `did:web:${base58}.${DOMAIN}`; 135 + } 136 + 137 + // Sign a JWT with ES256 using the local private key 138 + async function signJwtES256(payload, privateKey) { 139 + const header = { alg: 'ES256', typ: 'JWT' }; 140 + 141 + const b64url = (obj) => btoa(JSON.stringify(obj)) 142 + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); 143 + 144 + const encodedHeader = b64url(header); 145 + const encodedPayload = b64url(payload); 146 + const signingInput = `${encodedHeader}.${encodedPayload}`; 147 + 148 + const signature = await crypto.subtle.sign( 149 + { name: 'ECDSA', hash: 'SHA-256' }, 150 + privateKey, 151 + new TextEncoder().encode(signingInput) 152 + ); 153 + 154 + const encodedSignature = btoa(String.fromCharCode(...new Uint8Array(signature))) 155 + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); 156 + 157 + return `${signingInput}.${encodedSignature}`; 158 + } 159 + 160 + // Wire it all together replaces your nativeHS256 session builder 161 + async function createSelfSignedSession(obj) { 162 + const { privateKey, publicKeyJwk } = await getOrCreateKeypair(); 163 + const userDid = await publicKeyJwkToDidKey(publicKeyJwk); 164 + // userDid is now something like: did:key:zDnaerx9hy2RnqjkL... 165 + // This IS the username — deterministic, no server registration needed 166 + 167 + const now = Math.floor(Date.now() / 1000); 168 + const accessJwt = await signJwtES256(obj, privateKey); 169 + 170 + return accessJwt; 171 + //return { 172 + // did: userDid, 173 + // handle: `${userDid.slice(-8)}.${DOMAIN}`, // last 8 chars as handle 174 + // accessJwt, 175 + // // ...etc 176 + //}; 177 + } 178 + 179 + // In-memory cache to prevent redundant network lookups for service destinations 180 + const didServiceCache = new Map(); 181 + 182 + /** 183 + * Resolves an AT Protocol DID and extracts the correct service host URL. 184 + */ 185 + async function resolveServiceEndpoint(proxyHeaderValue) { 186 + if (!proxyHeaderValue) return null; 187 + 188 + // Split DID and fragment (e.g. "did:web:api.bsky.app" and "bsky_appview") 189 + const [did, fragmentId] = proxyHeaderValue.split('#'); 190 + if (!did) return null; 191 + 192 + // Check cache first 193 + const cacheKey = `${did}#${fragmentId || ''}`; 194 + if (didServiceCache.has(cacheKey)) { 195 + return didServiceCache.get(cacheKey); 196 + } 197 + 198 + try { 199 + let url; 200 + if (did.startsWith('did:web:')) { 201 + const domain = did.replace('did:web:', ''); 202 + url = `https://${domain}/.well-known/did.json`; 203 + } else if (did.startsWith('did:plc:')) { 204 + url = `https://plc.directory/${did}`; 205 + } else { 206 + return null; // Unsupported DID method 207 + } 208 + 209 + const res = await fetch(url); 210 + if (!res.ok) return null; 211 + const doc = await res.json(); 212 + 213 + if (!doc.service || !Array.isArray(doc.service)) return null; 214 + 215 + // Look for a matching service entry. 216 + // If no fragment was provided, default to the first available service. 217 + const service = doc.service.find(s => { 218 + if (!fragmentId) return true; 219 + return s.id === `#${fragmentId}` || s.id === `${did}#${fragmentId}`; 220 + }) || doc.service[0]; 221 + 222 + if (service && service.serviceEndpoint) { 223 + const endpoint = service.serviceEndpoint; 224 + didServiceCache.set(cacheKey, endpoint); 225 + return endpoint; 226 + } 227 + } catch (err) { 228 + console.error(`Failed to dynamically resolve DID: ${did}`, err); 229 + } 230 + 231 + return null; 232 + } 233 + 234 + // --- LOCAL STORAGE ATPROTO REPOSITORY CONTROLLERS --- 235 + async function getLocalRepoStore() { 236 + const data = await chrome.storage.local.get({ atproto_repo: {} }); 237 + return data.atproto_repo; 238 + } 239 + 240 + async function saveLocalRepoStore(store) { 241 + await chrome.storage.local.set({ atproto_repo: store }); 242 + } 243 + 244 + // Reusable function to attach with a built-in retry mechanism 245 + function attachAndEnableFetch(tabId, retries = 5, delay = 200) { 246 + chrome.debugger.attach({ tabId }, "1.3", () => { 247 + if (chrome.runtime.lastError) { 248 + const errorMsg = chrome.runtime.lastError.message; 249 + console.error(`Failed to attach to tab ${tabId}:`, errorMsg); 250 + 251 + if (retries > 0 && errorMsg.includes("Cannot access a chrome-extension")) { 252 + console.warn(`Chrome target engine is in limbo. Retrying attachment in ${delay}ms...`); 253 + setTimeout(() => { 254 + attachAndEnableFetch(tabId, retries - 1, delay * 2); 255 + }, delay); 256 + } else { 257 + if (attachedTabId === tabId) attachedTabId = null; 258 + } 259 + return; 260 + } 261 + 262 + attachedTabId = tabId; 263 + console.log(`Successfully attached and tracking tab ${tabId}`); 264 + 265 + chrome.debugger.sendCommand({ tabId }, "Fetch.enable", { 266 + patterns: [{ urlPattern: `*://${DOMAIN}/xrpc/*`, requestStage: "Request" }] 267 + }); 268 + }); 269 + } 270 + 271 + if (chrome.debugger) { 272 + async function manageDebugger(tabId) { 273 + try { 274 + const tab = await chrome.tabs.get(tabId).catch(() => null); 275 + if (!tab) return; 276 + 277 + const allowedUrls = ["bsky.app", DOMAIN]; 278 + const isMatched = allowedUrls.some(url => tab.url && tab.url.includes(url)); 279 + 280 + if (isMatched) { 281 + if (attachedTabId === tabId) return; 282 + 283 + if (attachedTabId && attachedTabId !== tabId) { 284 + await chrome.debugger.detach({ tabId: attachedTabId }).catch(() => {}); 285 + } 286 + 287 + attachAndEnableFetch(tabId); 288 + } else { 289 + if (attachedTabId) { 290 + const tabToDetach = attachedTabId; 291 + attachedTabId = null; 292 + chrome.debugger.detach({ tabId: tabToDetach }, () => { 293 + chrome.runtime.lastError; 294 + }); 295 + } 296 + } 297 + } catch (e) { 298 + console.error("Error managing debugger state:", e); 299 + } 300 + } 301 + 302 + chrome.debugger.onDetach.addListener((source, reason) => { 303 + if (reason === "canceled" && attachedTabId === source.tabId) attachedTabId = null; 304 + else if (reason === "target_closed") { 305 + if (attachedTabId === source.tabId) attachedTabId = null; 306 + chrome.tabs.get(source.tabId, (tab) => { 307 + if (!chrome.runtime.lastError && tab) attachAndEnableFetch(source.tabId); 308 + }); 309 + } 310 + }); 311 + 312 + chrome.tabs.onActivated.addListener((activeInfo) => manageDebugger(activeInfo.tabId)); 313 + chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { 314 + if (changeInfo.status === 'complete') manageDebugger(tabId); 315 + }); 316 + 317 + // --- CORE INTERCEPTION PIPELINE --- 318 + chrome.debugger.onEvent.addListener(async (source, method, params) => { 319 + if (method !== "Fetch.requestPaused") return; 320 + 321 + const { requestId, request } = params; 322 + const urlObj = new URL(request.url); 323 + const pathname = urlObj.pathname; 324 + const searchParams = urlObj.searchParams; 325 + 326 + // Standard baseline CORS Response Headers for standard web client interoperability 327 + const corsHeaders = [ 328 + { name: "Content-Type", value: "application/json" }, 329 + { name: "Access-Control-Allow-Origin", value: "*" }, 330 + { name: "Access-Control-Allow-Methods", value: "GET, POST, OPTIONS, PUT, DELETE" }, 331 + { name: "Access-Control-Allow-Headers", value: "*" } 332 + ]; 333 + 334 + try { 335 + // 1. Instantly skip and clear OPTIONS requests 336 + if (request.method === "OPTIONS") { 337 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 338 + requestId, responseCode: 200, responseHeaders: corsHeaders, body: safeBtoa("{}") 339 + }); 340 + return; 341 + } 342 + 343 + // We isolate traffic targeted at our mock domain's XRPC endpoint 344 + if (request.url.includes(`https://${DOMAIN}/xrpc/`)) { 345 + const xrpcMethod = pathname.split("/xrpc/")[1]; 346 + const mockedDid = `did:web:${DOMAIN}`; 347 + const { privateKey, publicKeyJwk } = await getOrCreateKeypair(); 348 + const userDid = await publicKeyJwkToDidKey(publicKeyJwk); 349 + const userId = userDid.split(":")[2].split(".")[0]; 350 + 351 + // Parse query input parameters or body payload context safely 352 + let queryParams = Object.fromEntries(searchParams.entries()); 353 + let bodyPayload = {}; 354 + if (request.postData) { 355 + try { bodyPayload = JSON.parse(request.postData); } catch (e) {} 356 + } 357 + 358 + // --- LOCAL ATPROTO METHOD MOCK GENERATORS --- 359 + 360 + // com.atproto.server.describeServer 361 + if (xrpcMethod === "com.atproto.server.describeServer") { 362 + const responseBody = { 363 + did: mockedDid, 364 + availableUserDomains: [`.${DOMAIN}`], 365 + inviteCodeRequired: false, 366 + phoneVerificationRequired: false, 367 + links: { privacyPolicy: `https://${DOMAIN}`, termsOfService: `https://${DOMAIN}` } 368 + }; 369 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 370 + requestId, responseCode: 200, responseHeaders: corsHeaders, body: safeBtoa(JSON.stringify(responseBody)) 371 + }); 372 + return; 373 + } 374 + 375 + // com.atproto.server.createSession 376 + if (xrpcMethod === "com.atproto.server.createSession" || xrpcMethod === "com.atproto.server.refreshSession" || xrpcMethod === "com.atproto.server.getSession") { 377 + const responseBody = { 378 + did: userDid, 379 + handle: `${userId}.${DOMAIN}`, 380 + email: `${userId}@${DOMAIN}`, 381 + emailConfirmed: true, 382 + emailAuthFactor: false, 383 + accessJwt: await createSelfSignedSession({ scope: "com.atproto.access", sub: userDid, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 7200, aud: mockedDid }), 384 + refreshJwt: await createSelfSignedSession({ scope: "com.atproto.refresh", sub: userDid, aud: mockedDid, jti: "mock-jti", iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 86400 }), 385 + active: true 386 + }; 387 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 388 + requestId, responseCode: 200, responseHeaders: corsHeaders, body: safeBtoa(JSON.stringify(responseBody)) 389 + }); 390 + return; 391 + } 392 + 393 + // com.atproto.repo.createRecord 394 + if (xrpcMethod === "com.atproto.repo.createRecord") { 395 + const { collection, record } = bodyPayload; 396 + const rkey = bodyPayload.rkey || "mock-" + Date.now(); 397 + const store = await getLocalRepoStore(); 398 + 399 + if (!store[collection]) store[collection] = {}; 400 + store[collection][rkey] = record; 401 + await saveLocalRepoStore(store); 402 + 403 + const responseBody = { uri: `at://${userDid}/${collection}/${rkey}`, cid: "mockCid123456789" }; 404 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 405 + requestId, responseCode: 200, responseHeaders: corsHeaders, body: safeBtoa(JSON.stringify(responseBody)) 406 + }); 407 + return; 408 + } 409 + 410 + if (xrpcMethod === "app.bsky.actor.getPreferences") { 411 + const { collection, rkey } = queryParams; 412 + const store = await getLocalRepoStore(); 413 + const record = store["app.bsky.actor.getPreferences"]; 414 + 415 + if (record) { 416 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 417 + requestId, responseCode: 200, responseHeaders: corsHeaders, 418 + body: safeBtoa(JSON.stringify(record)) 419 + }); 420 + } else { 421 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 422 + requestId, responseCode: 200, responseHeaders: corsHeaders, 423 + body: safeBtoa(JSON.stringify({"preferences": []})) 424 + }); 425 + } 426 + return; 427 + } 428 + 429 + if (xrpcMethod === "com.atproto.repo.putRecord") { 430 + 431 + const store = await getLocalRepoStore(); 432 + store["app.bsky.actor.getPreferences"] = bodyPayload; 433 + await saveLocalRepoStore(store); 434 + 435 + const responseBody = { uri: `at://${userDid}/${collection}/${rkey}`, cid: "mockCidUpdated9876" }; 436 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 437 + requestId, responseCode: 200, responseHeaders: corsHeaders, body: safeBtoa(JSON.stringify({})) 438 + }); 439 + return; 440 + } 441 + 442 + // com.atproto.repo.putRecord (Update / Replace Record Method) 443 + if (xrpcMethod === "com.atproto.repo.putRecord") { 444 + const { collection, rkey, record } = bodyPayload; 445 + const store = await getLocalRepoStore(); 446 + 447 + if (!store[collection]) store[collection] = {}; 448 + store[collection][rkey] = record; // Perform localized state update override 449 + await saveLocalRepoStore(store); 450 + 451 + const responseBody = { uri: `at://${userDid}/${collection}/${rkey}`, cid: "mockCidUpdated9876" }; 452 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 453 + requestId, responseCode: 200, responseHeaders: corsHeaders, body: safeBtoa(JSON.stringify(responseBody)) 454 + }); 455 + return; 456 + } 457 + 458 + // com.atproto.repo.getRecord 459 + if (xrpcMethod === "com.atproto.repo.getRecord") { 460 + const { collection, rkey } = queryParams; 461 + const store = await getLocalRepoStore(); 462 + const record = store[collection]?.[rkey]; 463 + 464 + if (record) { 465 + const responseBody = { uri: `at://${userDid}/${collection}/${rkey}`, cid: "mockCidFound", value: record }; 466 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 467 + requestId, responseCode: 200, responseHeaders: corsHeaders, body: safeBtoa(JSON.stringify(responseBody)) 468 + }); 469 + } else { 470 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 471 + requestId, responseCode: 404, responseHeaders: corsHeaders, 472 + body: safeBtoa(JSON.stringify({ error: "RecordNotFound", message: "Could not find record" })) 473 + }); 474 + } 475 + return; 476 + } 477 + 478 + // com.atproto.repo.deleteRecord 479 + if (xrpcMethod === "com.atproto.repo.deleteRecord") { 480 + const { collection, rkey } = bodyPayload; 481 + const store = await getLocalRepoStore(); 482 + 483 + if (store[collection] && store[collection][rkey]) { 484 + delete store[collection][rkey]; 485 + await saveLocalRepoStore(store); 486 + } 487 + 488 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 489 + requestId, responseCode: 200, responseHeaders: corsHeaders, body: safeBtoa(JSON.stringify({})) 490 + }); 491 + return; 492 + } 493 + 494 + // com.atproto.repo.listRecords 495 + if (xrpcMethod === "com.atproto.repo.listRecords") { 496 + const { collection } = queryParams; 497 + const store = await getLocalRepoStore(); 498 + const recordsForCollection = store[collection] || {}; 499 + 500 + const recordsArray = Object.entries(recordsForCollection).map(([rkey, value]) => ({ 501 + uri: `at://${userDid}/${collection}/${rkey}`, 502 + cid: "mockCidList", 503 + value: value 504 + })); 505 + 506 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 507 + requestId, responseCode: 200, responseHeaders: corsHeaders, body: safeBtoa(JSON.stringify({ records: recordsArray })) 508 + }); 509 + return; 510 + } 511 + 512 + if (xrpcMethod) { 513 + const proxyHeaderValue = request.headers ? (request.headers['atproto-proxy'] || '') : ''; 514 + 515 + // 1. Try to resolve the target destination from the header dynamically 516 + const resolvedTargetHost = await resolveServiceEndpoint(proxyHeaderValue); 517 + 518 + if (resolvedTargetHost) { 519 + // Universal AppView / Chat / Labeler Routing 520 + console.log(`🎯 Dynamically routing ${xrpcMethod} to resolved host: ${resolvedTargetHost}`); 521 + 522 + // Construct the destination URL using the dynamically discovered host 523 + const targetUrl = `${resolvedTargetHost.replace(/\/$/, '')}/xrpc/${xrpcMethod}${urlObj.search}`; 524 + 525 + const now = Math.floor(Date.now() / 1000); 526 + const serviceJwt = await signJwtES256({ 527 + iss: userDid, 528 + aud: proxyHeaderValue.split('#')[0], // already "did:web:api.bsky.chat#bsky_chat" 529 + lxm: xrpcMethod, 530 + iat: now, 531 + exp: now + 60, 532 + jti: crypto.randomUUID() 533 + }, privateKey); 534 + 535 + // Reconstruct and clean headers 536 + const cleanHeaders = {}; 537 + if (request.headers) { 538 + for (const [key, val] of Object.entries(request.headers)) { 539 + if (!['host', 'origin', 'referer', 'authorization'].includes(key.toLowerCase())) { 540 + cleanHeaders[key] = val; 541 + } 542 + } 543 + } 544 + // Maintain the proxy header so the downstream host accepts the query context 545 + cleanHeaders['authorization'] = `Bearer ${serviceJwt}`; 546 + cleanHeaders['atproto-proxy'] = proxyHeaderValue; 547 + 548 + const downstreamOptions = { 549 + method: request.method, 550 + headers: cleanHeaders 551 + }; 552 + 553 + if (request.method !== "GET" && request.method !== "HEAD" && request.postData) { 554 + downstreamOptions.body = request.postData; 555 + } 556 + 557 + try { 558 + const targetResponse = await fetch(targetUrl, downstreamOptions); 559 + const rawBodyText = await targetResponse.text(); 560 + 561 + const formattedResponseHeaders = []; 562 + targetResponse.headers.forEach((value, name) => { 563 + formattedResponseHeaders.push({ name, value }); 564 + }); 565 + 566 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 567 + requestId, 568 + responseCode: targetResponse.status, 569 + responseHeaders: formattedResponseHeaders, 570 + body: safeBtoa(rawBodyText) 571 + }); 572 + return; 573 + } catch (proxyError) { 574 + console.error("Dynamic Downstream Routing Failed:", proxyError); 575 + // Fallback on transport failure 576 + await chrome.debugger.sendCommand(source, "Fetch.continueRequest", { requestId }); 577 + return; 578 + } 579 + } 580 + } 581 + 582 + // --- FALLBACK INTERCEPT BACKEND FORWARDING (AppView Proxy Pipeline) --- 583 + // If it starts with app.bsky.* or isn't caught locally, seamlessly proxy to Bluesky production! 584 + //if (xrpcMethod.startsWith("app.bsky.")) { 585 + // console.log(`🔀 Proxying application view request downstream: ${xrpcMethod}`); 586 + // 587 + // // Construct the corresponding destination path to the live indexer 588 + // const appViewUrl = `https://api.bsky.app/xrpc/${xrpcMethod}${urlObj.search}`; 589 + // 590 + // // Cleanly extract incoming authorization headers to maintain user identity contexts 591 + // const cleanHeaders = {}; 592 + // if (request.headers) { 593 + // for (const [key, val] of Object.entries(request.headers)) { 594 + // // TREATED AS ANON TODO 595 + // if (!['host', 'origin', 'referer'].includes(key.toLowerCase())) { 596 + // cleanHeaders[key] = val; 597 + // } 598 + // } 599 + // } 600 + 601 + // const downstreamOptions = { 602 + // method: request.method, 603 + // headers: cleanHeaders 604 + // }; 605 + 606 + // if (request.method !== "GET" && request.method !== "HEAD" && request.postData) { 607 + // downstreamOptions.body = request.postData; 608 + // } 609 + 610 + // try { 611 + // const appViewResponse = await fetch(appViewUrl, downstreamOptions); 612 + // const rawBodyText = await appViewResponse.text(); 613 + 614 + // console.log(`🔀 DOING IT Proxying application view request downstream: ${xrpcMethod}`); 615 + // 616 + // // Map status codes and relay headers seamlessly back to client application 617 + // await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 618 + // requestId, 619 + // responseCode: appViewResponse.status, 620 + // responseHeaders: corsHeaders, 621 + // body: safeBtoa(rawBodyText) 622 + // }); 623 + // return; 624 + // } catch (proxyError) { 625 + // console.error("Downstream AppView Proxy Connection Failed:", proxyError); 626 + // await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 627 + // requestId, responseCode: 502, responseHeaders: corsHeaders, 628 + // body: safeBtoa(JSON.stringify({ error: "BadGateway", message: "Failed routing request to downstream AppView." })) 629 + // }); 630 + // return; 631 + // } 632 + //} 633 + } 634 + 635 + console.log(`DONT HAVE A HANDLER FOR THIS ${pathname}`); 636 + // If it doesn't match our proxy mock platform parameters, let the request proceed over standard internet paths 637 + await chrome.debugger.sendCommand(source, "Fetch.fulfillRequest", { 638 + requestId: requestId, 639 + responseCode: 404, 640 + responseHeaders: [ 641 + { name: "Content-Type", value: "application/json" }, 642 + { name: "Access-Control-Allow-Origin", value: "*" }, 643 + { name: "Access-Control-Allow-Methods", value: "GET, POST, OPTIONS, PUT, DELETE" }, 644 + { name: "Access-Control-Allow-Headers", value: "*" } 645 + ], 646 + body: safeBtoa(JSON.stringify({"error": "MethodNotFound", "message": "XRPC method not found"})) 647 + }); 648 + return; 649 + 650 + } catch (error) { 651 + if (error.message && error.message.includes("different extension")) { 652 + console.warn(`[Security Pass] Aborted request owned by another workspace extension.`); 653 + } else { 654 + console.error("Critical Runtime crash inside request handler intercept loop:", error); 655 + await chrome.debugger.sendCommand(source, "Fetch.continueRequest", { requestId }).catch(() => {}); 656 + } 657 + } 658 + }); 659 + } else { 660 + console.error("Debugger API missing. Ensure 'debugger' permission is configured inside your manifest.json."); 661 + }
+14
manifest.json
··· 1 + { 2 + "manifest_version": 3, 3 + "name": "MockIntercept POC", 4 + "version": "1.0", 5 + "permissions": ["debugger", "storage", "tabs", "notifications"], 6 + "host_permissions": ["<all_urls>"], 7 + "background": { 8 + "service_worker": "background.js" 9 + }, 10 + "options_page": "options.html", 11 + "action": { 12 + "default_title": "MockIntercept" 13 + } 14 + }
+32
options.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <title>MockIntercept Settings</title> 5 + <style> 6 + body { font-family: sans-serif; padding: 20px; line-height: 1.6; } 7 + textarea { width: 100%; height: 100px; font-family: monospace; } 8 + .section { margin-bottom: 20px; border-bottom: 1px solid #ccc; padding-bottom: 10px; } 9 + </style> 10 + </head> 11 + <body> 12 + <h1>MockIntercept POC</h1> 13 + 14 + <div class="section"> 15 + <h3>1. Allowed Domains (Watchlist)</h3> 16 + <p>Comma-separated list of sites to activate the plugin (e.g., localhost:3000, google.com)</p> 17 + <input type="text" id="allowedUrls" style="width: 100%;" placeholder="localhost:3000, my-dev-site.com"> 18 + </div> 19 + 20 + <div class="section"> 21 + <h3>2. Mock Rule</h3> 22 + <p>URL Fragment to intercept:</p> 23 + <input type="text" id="urlTrigger" placeholder="/api/v1/user"> 24 + <p>JSON Data to return:</p> 25 + <textarea id="mockData">{"status": "success", "data": "mocked"}</textarea> 26 + </div> 27 + 28 + <button id="save">Save Settings</button> 29 + 30 + <script src="options.js"></script> 31 + </body> 32 + </html>
+13
options.js
··· 1 + document.getElementById('save').addEventListener('click', () => { 2 + const allowedUrls = document.getElementById('allowedUrls').value.split(',').map(s => s.trim()); 3 + const urlTrigger = document.getElementById('urlTrigger').value; 4 + const mockData = JSON.parse(document.getElementById('mockData').value); 5 + 6 + // For the POC, we store one rule. You can expand this to an array later. 7 + chrome.storage.local.set({ 8 + allowedUrls, 9 + mockRules: [{ urlTrigger, mockData }] 10 + }, () => { 11 + alert('Settings saved! Refresh your target tab.'); 12 + }); 13 + });