From 925c4cec2fb76f7fea1c709a9960c838b826cdb9 Mon Sep 17 00:00:00 2001 From: seichris Date: Thu, 9 Oct 2025 14:45:51 +0200 Subject: [PATCH 1/5] add farcaster service --- cfg/tasks.json | 30 ++++++ src/manifest.json | 6 +- .../handlers/farcaster-username.ts | 98 +++++++++++++++++++ .../notarization/notarization-manager.ts | 2 + 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 src/side-panel/services/notarization/handlers/farcaster-username.ts diff --git a/cfg/tasks.json b/cfg/tasks.json index 34a76dc..804184d 100644 --- a/cfg/tasks.json +++ b/cfg/tasks.json @@ -148,6 +148,36 @@ "notarization": true } ] + }, + { + "id": "5", + "title": "Farcaster Username", + "service": "Farcaster", + "description": "Prove the ownership of a Farcaster account", + "icon": "https://farcaster.xyz/favicon.svg", + "permissionUrl": [ + "https://client.farcaster.xyz/v2/onboarding-state", + "https://farcaster.xyz" + ], + "groups": [ + { + "points": 10, + "semaphoreGroupId": "5", + "credentialGroupId": "6" + } + ], + "steps": [ + { + "text": "Visit website" + }, + { + "text": "Wait for request capture" + }, + { + "text": "MPC-TLS verification progress", + "notarization": true + } + ] } ] diff --git a/src/manifest.json b/src/manifest.json index 0b5f7a9..b4f1093 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "BringID", - "description": "Verify your Internet activity and prove that you’re a real human and not a bot.", + "description": "Verify your Internet activity and prove that you're a real human and not a bot.", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp76Uaolss4JYjXK1xc/5jG94VR3+5yLVvd0KV/mrtHWybG1eXXu384DHyJh0I3OSEjZgmnHqZR261b7zaOhDBzjeXLfQhHGBLJsE1I2QXTe5DNYQr91iDI3Tvd9fr8ScJ01gqJZjWC9YurXUYiOpJoQ6pandTAqP7s3nX6HTNDoVdZwjGiZHUN20pU6hU0I+d8wZH53CtkDPXRG9FkhlsliUaXaFIeCVp4m+O1xpxeYAv6Sb5UfXvM+9jRIhrnSw+aiwbDRhOqXPgkhIQdI0Bsan/9s78OEqvm4ats5mpCpwch0SAs9A6ysdQ28dmkyoj4V1lGKoIXOpsH/E/rw0PQIDAQAB", "background": { "service_worker": "background.bundle.js" @@ -50,7 +50,9 @@ "https://appleid.apple.com/account/manage/security/devices", "https://riders.uber.com/graphql", "https://riders.uber.com/trips", - "https://account.apple.com/account/manage/section/devices" + "https://account.apple.com/account/manage/section/devices", + "https://client.farcaster.xyz/*", + "https://farcaster.xyz/*" ], "host_permissions": [ "https://app.bringid.org/*" diff --git a/src/side-panel/services/notarization/handlers/farcaster-username.ts b/src/side-panel/services/notarization/handlers/farcaster-username.ts new file mode 100644 index 0000000..a071a1b --- /dev/null +++ b/src/side-panel/services/notarization/handlers/farcaster-username.ts @@ -0,0 +1,98 @@ +import { NotarizationBase } from '../notarization-base'; +import { RequestRecorder } from '../../requests-recorder'; +import { Request } from '../../../common/types'; +import { TLSNotary } from '../../tlsn'; +import { Commit } from 'tlsn-js'; +import { parse, Pointers, Mapping } from 'json-source-map'; + +export class NotarizationFarcasterUsername extends NotarizationBase { + requestRecorder: RequestRecorder = new RequestRecorder( + [ + { + method: 'GET', + urlPattern: 'https://client.farcaster.xyz/v2/onboarding-state', + }, + ], + this.onRequestsCaptured.bind(this), + ); + + public async onStart(): Promise { + this.requestRecorder.start(); + + await chrome.tabs.create({ url: 'https://farcaster.xyz' }); + + // check if on login page => this.setMessage('...') + this.currentStep = 1; + if (this.currentStepUpdateCallback) + this.currentStepUpdateCallback(this.currentStep); + } + + private async onRequestsCaptured(log: Array) { + this.currentStep = 2; + if (this.currentStepUpdateCallback) + this.currentStepUpdateCallback(this.currentStep); + + try { + const notary = await TLSNotary.new( + { + serverDns: 'client.farcaster.xyz', + maxSentData: 2048, + maxRecvData: 8192, + }, + { + logEveryNMessages: 100, + verbose: true, + logPrefix: '[WS Monitor / Farcaster-Username]', + trackSize: true, + expectedTotalBytes: 55000000 * 1.15, + enableProgress: true, + progressUpdateInterval: 500, + }, + ); + + delete log[0].headers['Accept-Encoding']; + + const result = await notary.transcript(log[0]); + if (result instanceof Error) { + this.result(result); + return; + } + const [transcript, message] = result; + + const commit: Commit = { + sent: [{ start: 0, end: transcript.sent.length }], + recv: [{ start: 0, end: message.info.length }], + }; + + const jsonStarts: number = Buffer.from(transcript.recv) + .toString('utf-8') + .indexOf('{'); + + const pointers: Pointers = parse(message.body.toString()).pointers; + + const username: Mapping = pointers['/result/state/user/username']; + console.log({ pointers }); + + if (!username.key?.pos) { + this.result(new Error('username not found')); + return; + } + + commit.recv.push({ + start: jsonStarts + username.key?.pos, + end: jsonStarts + username.valueEnd.pos, + }); + + console.log({ commit }); + + this.result(await notary.notarize(commit)); + } catch (err) { + this.result(err as Error); + } + } + + public async onStop(): Promise { + this.requestRecorder.stop(); + } +} + diff --git a/src/side-panel/services/notarization/notarization-manager.ts b/src/side-panel/services/notarization/notarization-manager.ts index 56c9f61..5784f59 100644 --- a/src/side-panel/services/notarization/notarization-manager.ts +++ b/src/side-panel/services/notarization/notarization-manager.ts @@ -9,6 +9,7 @@ import { store } from '../../store'; import { NotarizationStravaPremium } from './handlers/strava-premium'; import { NotarizationAppleDevices } from './handlers/apple-devices'; import { NotarizationXVerifiedFollowers } from './handlers/x-verified-followers'; +import { NotarizationFarcasterUsername } from './handlers/farcaster-username'; // NotarizationManager stores Notarization and handles Redux export class NotarizationManager { @@ -85,4 +86,5 @@ export const notarizationManager = new NotarizationManager([ new NotarizationUberRides(t[1]), new NotarizationXVerifiedFollowers(t[2]), new NotarizationAppleDevices(t[3]), + new NotarizationFarcasterUsername(t[4]), ]); From 7805506a7bc17b82007a8d9fff9814beec7c61ea Mon Sep 17 00:00:00 2001 From: seichris Date: Thu, 9 Oct 2025 15:30:09 +0200 Subject: [PATCH 2/5] fix permission error from using asterisk --- src/manifest.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/manifest.json b/src/manifest.json index b4f1093..51cd2f8 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -51,8 +51,9 @@ "https://riders.uber.com/graphql", "https://riders.uber.com/trips", "https://account.apple.com/account/manage/section/devices", - "https://client.farcaster.xyz/*", - "https://farcaster.xyz/*" + "https://client.farcaster.xyz/v2/onboarding-state", + "https://farcaster.xyz" + ], "host_permissions": [ "https://app.bringid.org/*" From 8c1fef303659922a3b88a92ac3957524cdeb40c7 Mon Sep 17 00:00:00 2001 From: seichris Date: Thu, 9 Oct 2025 15:35:17 +0200 Subject: [PATCH 3/5] fix permission error from using asterisk - add asterisk again --- cfg/tasks.json | 2 +- src/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cfg/tasks.json b/cfg/tasks.json index 804184d..700776f 100644 --- a/cfg/tasks.json +++ b/cfg/tasks.json @@ -157,7 +157,7 @@ "icon": "https://farcaster.xyz/favicon.svg", "permissionUrl": [ "https://client.farcaster.xyz/v2/onboarding-state", - "https://farcaster.xyz" + "https://farcaster.xyz/*" ], "groups": [ { diff --git a/src/manifest.json b/src/manifest.json index 51cd2f8..624ee4a 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -52,7 +52,7 @@ "https://riders.uber.com/trips", "https://account.apple.com/account/manage/section/devices", "https://client.farcaster.xyz/v2/onboarding-state", - "https://farcaster.xyz" + "https://farcaster.xyz/*" ], "host_permissions": [ From c86041baf056353eecd780f5700425c2c64c71a2 Mon Sep 17 00:00:00 2001 From: seichris Date: Sat, 11 Oct 2025 10:18:56 +0200 Subject: [PATCH 4/5] feat: Instead of prove of account, you now prove you're followed by notable accounts like vitalik.eth, v, dwr.eth, jessepollak... --- cfg/tasks.json | 5 +- src/content/farcaster-helper.tsx | 49 +++ src/manifest.json | 8 + .../handlers/farcaster-legit-followers.ts | 332 ++++++++++++++++++ ...ername.ts => farcaster-username-legacy.ts} | 0 .../notarization/notarization-manager.ts | 4 +- .../utils/check-if-permission-granted.tsx | 26 +- src/side-panel/utils/request-permission.tsx | 26 +- webpack.config.js | 3 +- 9 files changed, 446 insertions(+), 7 deletions(-) create mode 100644 src/content/farcaster-helper.tsx create mode 100644 src/side-panel/services/notarization/handlers/farcaster-legit-followers.ts rename src/side-panel/services/notarization/handlers/{farcaster-username.ts => farcaster-username-legacy.ts} (100%) diff --git a/cfg/tasks.json b/cfg/tasks.json index 700776f..38603a9 100644 --- a/cfg/tasks.json +++ b/cfg/tasks.json @@ -151,12 +151,13 @@ }, { "id": "5", - "title": "Farcaster Username", + "title": "Farcaster Legit Followers", "service": "Farcaster", - "description": "Prove the ownership of a Farcaster account", + "description": "Prove you're followed by notable accounts like vitalik.eth, v, dwr.eth, jessepollak, or balajis.eth", "icon": "https://farcaster.xyz/favicon.svg", "permissionUrl": [ "https://client.farcaster.xyz/v2/onboarding-state", + "https://client.farcaster.xyz/v2/followers*", "https://farcaster.xyz/*" ], "groups": [ diff --git a/src/content/farcaster-helper.tsx b/src/content/farcaster-helper.tsx new file mode 100644 index 0000000..4f0bcb1 --- /dev/null +++ b/src/content/farcaster-helper.tsx @@ -0,0 +1,49 @@ +// Content script for Farcaster that listens for messages to fetch followers +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'FETCH_FARCASTER_FOLLOWERS') { + const { fid, targetUsernames } = message.payload; + + // Function to fetch all followers pages until we find one of our target usernames or exhaust all pages + const fetchFollowersUntilTarget = async (cursor: string | null = null): Promise => { + try { + const url = cursor + ? `https://client.farcaster.xyz/v2/followers?fid=${fid}&limit=100&cursor=${cursor}` + : `https://client.farcaster.xyz/v2/followers?fid=${fid}&limit=100`; + + const response = await fetch(url); + const data = await response.json(); + + console.log(`[Farcaster] Fetched followers page, got ${data?.result?.users?.length || 0} users`); + + // Check if any of our target usernames are in this page + const foundUser = data?.result?.users?.find((user: any) => + targetUsernames.includes(user.username) + ); + + if (foundUser) { + console.log(`[Farcaster] Found ${foundUser.username} in this page!`); + return; + } + + // If there's a next cursor and we haven't found a target, fetch next page + if (data?.next?.cursor) { + console.log(`[Farcaster] Target usernames not found yet, fetching next page...`); + await fetchFollowersUntilTarget(data.next.cursor); + } else { + console.log(`[Farcaster] Reached end of followers list, none of [${targetUsernames.join(', ')}] found`); + } + } catch (err) { + console.error('[Farcaster] Error fetching followers:', err); + } + }; + + fetchFollowersUntilTarget().then(() => { + sendResponse({ success: true }); + }).catch((err) => { + sendResponse({ success: false, error: err.message }); + }); + + return true; // Keep message channel open for async response + } +}); + diff --git a/src/manifest.json b/src/manifest.json index 624ee4a..2a70e2f 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -27,6 +27,13 @@ ], "js": ["contentScript.bundle.js"], "css": [] + }, + { + "matches": [ + "https://farcaster.xyz/*" + ], + "js": ["farcasterHelper.bundle.js"], + "css": [] } ], "externally_connectable": { @@ -52,6 +59,7 @@ "https://riders.uber.com/trips", "https://account.apple.com/account/manage/section/devices", "https://client.farcaster.xyz/v2/onboarding-state", + "https://client.farcaster.xyz/v2/followers*", "https://farcaster.xyz/*" ], diff --git a/src/side-panel/services/notarization/handlers/farcaster-legit-followers.ts b/src/side-panel/services/notarization/handlers/farcaster-legit-followers.ts new file mode 100644 index 0000000..fb03bdd --- /dev/null +++ b/src/side-panel/services/notarization/handlers/farcaster-legit-followers.ts @@ -0,0 +1,332 @@ +import { NotarizationBase } from '../notarization-base'; +import { RequestRecorder } from '../../requests-recorder'; +import { Request } from '../../../common/types'; +import { TLSNotary } from '../../tlsn'; +import { Commit } from 'tlsn-js'; +import { parse, Pointers, Mapping } from 'json-source-map'; + +export class NotarizationFarcasterLegitFollowers extends NotarizationBase { + // Configure target usernames to check for (easily add more here) + private readonly TARGET_USERNAMES = [ + 'vitalik.eth', // THE vitalik + 'v', // Varun Srinivasan (Farcaster co-founder) + 'dwr.eth', // Dan Romero (Farcaster co-founder) + 'jessepollak', // Jesse Pollak (Base) + 'balajis.eth', // Balaji Srinivasan + ]; + + private capturedRequests: Array = []; + private userFid: number | null = null; + private currentTabId: number | null = null; + private foundUsername: string | null = null; + + requestRecorder: RequestRecorder = new RequestRecorder( + [ + { + method: 'GET', + urlPattern: 'https://client.farcaster.xyz/v2/onboarding-state', + }, + { + method: 'GET', + urlPattern: 'https://client.farcaster.xyz/v2/followers*', + }, + ], + this.onRequestsCaptured.bind(this), + ); + + public async onStart(): Promise { + this.requestRecorder.start(); + + const tab = await chrome.tabs.create({ url: 'https://farcaster.xyz' }); + this.currentTabId = tab.id || null; + + // check if on login page => this.setMessage('...') + this.currentStep = 1; + if (this.currentStepUpdateCallback) + this.currentStepUpdateCallback(this.currentStep); + } + + private async onRequestsCaptured(log: Array) { + // Store captured requests + this.capturedRequests.push(...log); + + // Check if we have the onboarding-state request + const onboardingRequest = this.capturedRequests.find( + req => req.url.includes('/onboarding-state') + ); + + // Check if we already have FID and need to trigger followers request + if (onboardingRequest && !this.userFid) { + // We need to process the transcript to get the FID + try { + const notary = await TLSNotary.new( + { + serverDns: 'client.farcaster.xyz', + maxSentData: 2048, + maxRecvData: 8192, + }, + { + logEveryNMessages: 100, + verbose: true, + logPrefix: '[WS Monitor / Farcaster-Legit-Followers-FID-Extract]', + trackSize: true, + expectedTotalBytes: 55000000 * 1.15, + enableProgress: false, + progressUpdateInterval: 500, + }, + ); + + const reqCopy = { ...onboardingRequest }; + delete reqCopy.headers['Accept-Encoding']; + + const result = await notary.transcript(reqCopy); + if (!(result instanceof Error)) { + const [, message] = result; + const responseData = JSON.parse(message.body.toString()); + this.userFid = responseData?.result?.state?.user?.fid; + + if (this.userFid) { + console.log(`[Farcaster] Found user FID: ${this.userFid}`); + await this.triggerFollowersRequest(this.userFid); + } + } + } catch (err) { + console.error('[Farcaster] Error extracting FID:', err); + } + } + + // Check if we have the followers requests + const followersRequests = this.capturedRequests.filter( + req => req.url.includes('/followers?fid=') + ); + + if (onboardingRequest && followersRequests.length > 0) { + // Find which followers request contains one of our target usernames + const followersRequestWithTarget = await this.findFollowersRequestWithTargetUsername(followersRequests); + + if (followersRequestWithTarget) { + // We have both requests, proceed with notarization + await this.processNotarization(onboardingRequest, followersRequestWithTarget); + } + } + } + + private async findFollowersRequestWithTargetUsername( + followersRequests: Array + ): Promise { + // Check each followers request to see which one contains one of our target usernames + for (const request of followersRequests) { + try { + const notary = await TLSNotary.new( + { + serverDns: 'client.farcaster.xyz', + maxSentData: 2048, + maxRecvData: 16384, + }, + { + logEveryNMessages: 100, + verbose: false, + logPrefix: '[WS Monitor / Farcaster-Check-Target]', + trackSize: false, + expectedTotalBytes: 55000000 * 1.15, + enableProgress: false, + progressUpdateInterval: 500, + }, + ); + + const reqCopy = { ...request }; + delete reqCopy.headers['Accept-Encoding']; + + const result = await notary.transcript(reqCopy); + if (!(result instanceof Error)) { + const [, message] = result; + const responseData = JSON.parse(message.body.toString()); + const users = responseData?.result?.users || []; + + // Check if any of our target usernames are in this page + for (const targetUsername of this.TARGET_USERNAMES) { + const foundUser = users.find((user: any) => user.username === targetUsername); + if (foundUser) { + this.foundUsername = targetUsername; + console.log(`[Farcaster] Found the followers request containing ${targetUsername}`); + return request; + } + } + } + } catch (err) { + console.error('[Farcaster] Error checking request for target usernames:', err); + } + } + + console.log(`[Farcaster] None of the target usernames (${this.TARGET_USERNAMES.join(', ')}) found in captured followers requests`); + return null; + } + + private async triggerFollowersRequest(fid: number): Promise { + // Send a message to the Farcaster content script to fetch followers + if (this.currentTabId) { + try { + await chrome.tabs.sendMessage(this.currentTabId, { + type: 'FETCH_FARCASTER_FOLLOWERS', + payload: { + fid, + targetUsernames: this.TARGET_USERNAMES, + }, + }); + console.log('[Farcaster] Sent message to content script to fetch followers'); + } catch (err) { + console.error('[Farcaster] Error sending message to content script:', err); + } + } + } + + private async processNotarization( + onboardingRequest: Request, + followersRequest: Request + ): Promise { + this.currentStep = 2; + if (this.currentStepUpdateCallback) + this.currentStepUpdateCallback(this.currentStep); + + try { + const notary = await TLSNotary.new( + { + serverDns: 'client.farcaster.xyz', + maxSentData: 4096, + maxRecvData: 16384, + }, + { + logEveryNMessages: 100, + verbose: true, + logPrefix: '[WS Monitor / Farcaster-Legit-Followers]', + trackSize: true, + expectedTotalBytes: 55000000 * 1.15, + enableProgress: true, + progressUpdateInterval: 500, + }, + ); + + // Process onboarding-state request for username + delete onboardingRequest.headers['Accept-Encoding']; + + const result1 = await notary.transcript(onboardingRequest); + if (result1 instanceof Error) { + this.result(result1); + return; + } + const [transcript1, message1] = result1; + + const commit: Commit = { + sent: [{ start: 0, end: transcript1.sent.length }], + recv: [{ start: 0, end: message1.info.length }], + }; + + const jsonStarts1: number = Buffer.from(transcript1.recv) + .toString('utf-8') + .indexOf('{'); + + const pointers1: Pointers = parse(message1.body.toString()).pointers; + + const username: Mapping = pointers1['/result/state/user/username']; + const fid: Mapping = pointers1['/result/state/user/fid']; + console.log({ pointers: pointers1 }); + + if (!username.key?.pos) { + this.result(new Error('username not found')); + return; + } + + if (!fid.key?.pos) { + this.result(new Error('fid not found')); + return; + } + + // Commit username and fid from onboarding-state + commit.recv.push({ + start: jsonStarts1 + username.key?.pos, + end: jsonStarts1 + username.valueEnd.pos, + }); + + commit.recv.push({ + start: jsonStarts1 + fid.key?.pos, + end: jsonStarts1 + fid.valueEnd.pos, + }); + + // Process followers request to check for vitalik.eth + delete followersRequest.headers['Accept-Encoding']; + + const result2 = await notary.transcript(followersRequest); + if (result2 instanceof Error) { + this.result(result2); + return; + } + const [transcript2, message2] = result2; + + const jsonStarts2: number = Buffer.from(transcript2.recv) + .toString('utf-8') + .indexOf('{'); + + const followersData = JSON.parse(message2.body.toString()); + const pointers2: Pointers = parse(message2.body.toString()).pointers; + + // Check if our target username is in the followers list + let targetFollowerIndex = -1; + const users = followersData?.result?.users || []; + + // Use the username we found earlier, or fall back to checking all targets + const usernameToFind = this.foundUsername || this.TARGET_USERNAMES[0]; + + for (let i = 0; i < users.length; i++) { + if (this.TARGET_USERNAMES.includes(users[i].username)) { + targetFollowerIndex = i; + this.foundUsername = users[i].username; + break; + } + } + + if (targetFollowerIndex === -1) { + this.result(new Error(`None of the target usernames (${this.TARGET_USERNAMES.join(', ')}) are following this user`)); + return; + } + + console.log(`[Farcaster] Found ${this.foundUsername} at followers index ${targetFollowerIndex}`); + + // Commit the target follower entry + const targetUsername: Mapping = pointers2[`/result/users/${targetFollowerIndex}/username`]; + const targetFollowedBy: Mapping = pointers2[`/result/users/${targetFollowerIndex}/viewerContext/followedBy`]; + + if (!targetUsername.key?.pos) { + this.result(new Error(`${this.foundUsername} username pointer not found`)); + return; + } + + if (!targetFollowedBy.key?.pos) { + this.result(new Error(`${this.foundUsername} followedBy pointer not found`)); + return; + } + + // Commit target username + commit.recv.push({ + start: jsonStarts2 + targetUsername.key?.pos, + end: jsonStarts2 + targetUsername.valueEnd.pos, + }); + + // Commit followedBy status (should be true) + commit.recv.push({ + start: jsonStarts2 + targetFollowedBy.key?.pos, + end: jsonStarts2 + targetFollowedBy.valueEnd.pos, + }); + + console.log({ commit }); + + this.result(await notary.notarize(commit)); + } catch (err) { + this.result(err as Error); + } + } + + public async onStop(): Promise { + this.requestRecorder.stop(); + } +} + diff --git a/src/side-panel/services/notarization/handlers/farcaster-username.ts b/src/side-panel/services/notarization/handlers/farcaster-username-legacy.ts similarity index 100% rename from src/side-panel/services/notarization/handlers/farcaster-username.ts rename to src/side-panel/services/notarization/handlers/farcaster-username-legacy.ts diff --git a/src/side-panel/services/notarization/notarization-manager.ts b/src/side-panel/services/notarization/notarization-manager.ts index 5784f59..7073a3c 100644 --- a/src/side-panel/services/notarization/notarization-manager.ts +++ b/src/side-panel/services/notarization/notarization-manager.ts @@ -9,7 +9,7 @@ import { store } from '../../store'; import { NotarizationStravaPremium } from './handlers/strava-premium'; import { NotarizationAppleDevices } from './handlers/apple-devices'; import { NotarizationXVerifiedFollowers } from './handlers/x-verified-followers'; -import { NotarizationFarcasterUsername } from './handlers/farcaster-username'; +import { NotarizationFarcasterLegitFollowers } from './handlers/farcaster-legit-followers'; // NotarizationManager stores Notarization and handles Redux export class NotarizationManager { @@ -86,5 +86,5 @@ export const notarizationManager = new NotarizationManager([ new NotarizationUberRides(t[1]), new NotarizationXVerifiedFollowers(t[2]), new NotarizationAppleDevices(t[3]), - new NotarizationFarcasterUsername(t[4]), + new NotarizationFarcasterLegitFollowers(t[4]), ]); diff --git a/src/side-panel/utils/check-if-permission-granted.tsx b/src/side-panel/utils/check-if-permission-granted.tsx index 28f6056..d722255 100644 --- a/src/side-panel/utils/check-if-permission-granted.tsx +++ b/src/side-panel/utils/check-if-permission-granted.tsx @@ -1,8 +1,32 @@ +/** + * Normalizes a permission URL to ensure it has the correct format for Chrome's permissions API. + * - If URL ends with a TLD (e.g., .com, .xyz) without a path, appends "/*" + * - If URL already has a path or wildcard, leaves it unchanged + */ +const normalizePermissionUrl = (url: string): string => { + try { + const urlObj = new URL(url); + // If the pathname is just "/" or empty, append "*" + if (urlObj.pathname === '/' || urlObj.pathname === '') { + return url.endsWith('/') ? `${url}*` : `${url}/*`; + } + // If already has a wildcard or specific path, return as-is + return url; + } catch (e) { + // If URL parsing fails, return as-is + console.warn('Failed to parse permission URL:', url, e); + return url; + } +}; + const checkIfPermissionGranted = async ( origins: string[], ): Promise => { + // Normalize all URLs before checking permissions + const normalizedOrigins = origins.map(normalizePermissionUrl); + return new Promise((resolve) => { - chrome.permissions.contains({ origins }, (result) => { + chrome.permissions.contains({ origins: normalizedOrigins }, (result) => { if (chrome.runtime.lastError) { console.error( 'Permission check error:', diff --git a/src/side-panel/utils/request-permission.tsx b/src/side-panel/utils/request-permission.tsx index 9edceb6..eb8cab9 100644 --- a/src/side-panel/utils/request-permission.tsx +++ b/src/side-panel/utils/request-permission.tsx @@ -1,6 +1,30 @@ +/** + * Normalizes a permission URL to ensure it has the correct format for Chrome's permissions API. + * - If URL ends with a TLD (e.g., .com, .xyz) without a path, appends "/*" + * - If URL already has a path or wildcard, leaves it unchanged + */ +const normalizePermissionUrl = (url: string): string => { + try { + const urlObj = new URL(url); + // If the pathname is just "/" or empty, append "*" + if (urlObj.pathname === '/' || urlObj.pathname === '') { + return url.endsWith('/') ? `${url}*` : `${url}/*`; + } + // If already has a wildcard or specific path, return as-is + return url; + } catch (e) { + // If URL parsing fails, return as-is + console.warn('Failed to parse permission URL:', url, e); + return url; + } +}; + const requestHostPermission = async (origins: string[]): Promise => { + // Normalize all URLs before requesting permissions + const normalizedOrigins = origins.map(normalizePermissionUrl); + return new Promise((resolve) => { - chrome.permissions.request({ origins }, (granted) => { + chrome.permissions.request({ origins: normalizedOrigins }, (granted) => { if (chrome.runtime.lastError) { console.error( 'Permission request error:', diff --git a/webpack.config.js b/webpack.config.js index bb35806..be2068a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -45,7 +45,8 @@ const options = { // should be injected to webpage contentScript: path.join(__dirname, "src", "content", "index.tsx"), - content: path.join(__dirname, "src", "content", "content.tsx") + content: path.join(__dirname, "src", "content", "content.tsx"), + farcasterHelper: path.join(__dirname, "src", "content", "farcaster-helper.tsx") }, output: { filename: "[name].bundle.js", From 480c7a86d1b877ce10bdebb223119ecb53d3d90d Mon Sep 17 00:00:00 2001 From: seichris Date: Mon, 13 Oct 2025 12:11:48 +0400 Subject: [PATCH 5/5] fix: use scripting to send request with auth headers --- src/content/farcaster-helper.tsx | 49 --- src/manifest.json | 10 +- .../handlers/farcaster-legit-followers.ts | 353 +++++++++--------- webpack.config.js | 3 +- 4 files changed, 172 insertions(+), 243 deletions(-) delete mode 100644 src/content/farcaster-helper.tsx diff --git a/src/content/farcaster-helper.tsx b/src/content/farcaster-helper.tsx deleted file mode 100644 index 4f0bcb1..0000000 --- a/src/content/farcaster-helper.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// Content script for Farcaster that listens for messages to fetch followers -chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.type === 'FETCH_FARCASTER_FOLLOWERS') { - const { fid, targetUsernames } = message.payload; - - // Function to fetch all followers pages until we find one of our target usernames or exhaust all pages - const fetchFollowersUntilTarget = async (cursor: string | null = null): Promise => { - try { - const url = cursor - ? `https://client.farcaster.xyz/v2/followers?fid=${fid}&limit=100&cursor=${cursor}` - : `https://client.farcaster.xyz/v2/followers?fid=${fid}&limit=100`; - - const response = await fetch(url); - const data = await response.json(); - - console.log(`[Farcaster] Fetched followers page, got ${data?.result?.users?.length || 0} users`); - - // Check if any of our target usernames are in this page - const foundUser = data?.result?.users?.find((user: any) => - targetUsernames.includes(user.username) - ); - - if (foundUser) { - console.log(`[Farcaster] Found ${foundUser.username} in this page!`); - return; - } - - // If there's a next cursor and we haven't found a target, fetch next page - if (data?.next?.cursor) { - console.log(`[Farcaster] Target usernames not found yet, fetching next page...`); - await fetchFollowersUntilTarget(data.next.cursor); - } else { - console.log(`[Farcaster] Reached end of followers list, none of [${targetUsernames.join(', ')}] found`); - } - } catch (err) { - console.error('[Farcaster] Error fetching followers:', err); - } - }; - - fetchFollowersUntilTarget().then(() => { - sendResponse({ success: true }); - }).catch((err) => { - sendResponse({ success: false, error: err.message }); - }); - - return true; // Keep message channel open for async response - } -}); - diff --git a/src/manifest.json b/src/manifest.json index 2a70e2f..b4d9000 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -27,13 +27,6 @@ ], "js": ["contentScript.bundle.js"], "css": [] - }, - { - "matches": [ - "https://farcaster.xyz/*" - ], - "js": ["farcasterHelper.bundle.js"], - "css": [] } ], "externally_connectable": { @@ -71,6 +64,7 @@ "storage", "webRequest", "activeTab", - "sidePanel" + "sidePanel", + "scripting" ] } diff --git a/src/side-panel/services/notarization/handlers/farcaster-legit-followers.ts b/src/side-panel/services/notarization/handlers/farcaster-legit-followers.ts index fb03bdd..c834be6 100644 --- a/src/side-panel/services/notarization/handlers/farcaster-legit-followers.ts +++ b/src/side-panel/services/notarization/handlers/farcaster-legit-followers.ts @@ -15,99 +15,173 @@ export class NotarizationFarcasterLegitFollowers extends NotarizationBase { 'balajis.eth', // Balaji Srinivasan ]; - private capturedRequests: Array = []; private userFid: number | null = null; + private username: string | null = null; private currentTabId: number | null = null; private foundUsername: string | null = null; + // First recorder: only for onboarding-state requestRecorder: RequestRecorder = new RequestRecorder( [ { method: 'GET', urlPattern: 'https://client.farcaster.xyz/v2/onboarding-state', }, - { - method: 'GET', - urlPattern: 'https://client.farcaster.xyz/v2/followers*', - }, ], - this.onRequestsCaptured.bind(this), + this.onOnboardingStateCaptured.bind(this), ); + + // Second recorder: for followers (started after we get FID) + private followersRecorder: RequestRecorder | null = null; public async onStart(): Promise { + console.log('[Farcaster] onStart called'); + this.requestRecorder.start(); + console.log('[Farcaster] Listening for onboarding-state...'); const tab = await chrome.tabs.create({ url: 'https://farcaster.xyz' }); this.currentTabId = tab.id || null; + console.log('[Farcaster] Tab created with ID:', this.currentTabId); - // check if on login page => this.setMessage('...') this.currentStep = 1; if (this.currentStepUpdateCallback) this.currentStepUpdateCallback(this.currentStep); } - private async onRequestsCaptured(log: Array) { - // Store captured requests - this.capturedRequests.push(...log); + private async onOnboardingStateCaptured(log: Array) { + console.log('[Farcaster] ✅ Onboarding-state captured!', log[0].url); - // Check if we have the onboarding-state request - const onboardingRequest = this.capturedRequests.find( - req => req.url.includes('/onboarding-state') - ); + // Extract auth header from the captured request + const authHeader = log[0].headers['Authorization'] || log[0].headers['authorization']; - // Check if we already have FID and need to trigger followers request - if (onboardingRequest && !this.userFid) { - // We need to process the transcript to get the FID - try { - const notary = await TLSNotary.new( - { - serverDns: 'client.farcaster.xyz', - maxSentData: 2048, - maxRecvData: 8192, - }, + if (!authHeader) { + this.result(new Error('No Authorization header found in captured request')); + return; + } + + console.log('[Farcaster] Found auth header, fetching FID...'); + + try { + if (!this.currentTabId) { + this.result(new Error('No tab ID available')); + return; + } + + // Use the auth header from the captured request + const results = await chrome.scripting.executeScript({ + target: { tabId: this.currentTabId }, + args: [authHeader], + func: async (auth: string) => { + console.log('[executeScript] Fetching with auth...'); + const response = await fetch('https://client.farcaster.xyz/v2/onboarding-state', { + headers: { + 'Authorization': auth + } + }); + console.log('[executeScript] Response status:', response.status); + const data = await response.json(); + console.log('[executeScript] Response data:', data); + return { + fid: data?.result?.state?.user?.fid, + username: data?.result?.state?.user?.username, + }; + }, + }); + + console.log('[Farcaster] executeScript results:', results); + + if (!results || results.length === 0 || !results[0].result) { + this.result(new Error('Failed to execute script to get FID')); + return; + } + + const { fid, username } = results[0].result; + this.userFid = fid; + this.username = username; + + console.log(`[Farcaster] Extracted FID: ${this.userFid}, username: ${this.username}`); + + if (!this.userFid || !this.username) { + this.result(new Error('Could not extract FID and username from page')); + return; + } + + // Now start listening for followers requests + console.log('[Farcaster] Starting followers recorder...'); + this.followersRecorder = new RequestRecorder( + [ { - logEveryNMessages: 100, - verbose: true, - logPrefix: '[WS Monitor / Farcaster-Legit-Followers-FID-Extract]', - trackSize: true, - expectedTotalBytes: 55000000 * 1.15, - enableProgress: false, - progressUpdateInterval: 500, + method: 'GET', + urlPattern: 'https://client.farcaster.xyz/v2/followers*', }, - ); - - const reqCopy = { ...onboardingRequest }; - delete reqCopy.headers['Accept-Encoding']; - - const result = await notary.transcript(reqCopy); - if (!(result instanceof Error)) { - const [, message] = result; - const responseData = JSON.parse(message.body.toString()); - this.userFid = responseData?.result?.state?.user?.fid; - - if (this.userFid) { - console.log(`[Farcaster] Found user FID: ${this.userFid}`); - await this.triggerFollowersRequest(this.userFid); - } - } - } catch (err) { - console.error('[Farcaster] Error extracting FID:', err); + ], + this.onFollowersCaptured.bind(this), + ); + this.followersRecorder.start(); + + // Fetch followers directly from side panel (no auth needed for this endpoint!) + console.log(`[Farcaster] Starting to fetch followers for FID ${this.userFid}...`); + await this.fetchFollowersUntilTarget(this.userFid); + + } catch (err) { + console.error('[Farcaster] Error processing onboarding-state:', err); + this.result(err as Error); + } + } + + private async fetchFollowersUntilTarget(userFid: number, cursor: string | null = null): Promise { + try { + const url = cursor + ? `https://client.farcaster.xyz/v2/followers?fid=${userFid}&limit=100&cursor=${cursor}` + : `https://client.farcaster.xyz/v2/followers?fid=${userFid}&limit=100`; + + console.log(`[Farcaster] Fetching followers from: ${url}`); + const response = await fetch(url); + console.log(`[Farcaster] Response status: ${response.status}`); + const data = await response.json(); + + console.log(`[Farcaster] Fetched followers page, got ${data?.result?.users?.length || 0} users`); + + // Check if any of our target usernames are in this page + const foundUser = data?.result?.users?.find((user: any) => + this.TARGET_USERNAMES.includes(user.username) + ); + + if (foundUser) { + console.log(`[Farcaster] ✅ FOUND ${foundUser.username} in this page!`); + // Don't fetch more - RequestRecorder will capture this request + // and onFollowersCaptured will handle notarization + return; + } + + // If there's a next cursor and we haven't found a target, fetch next page + if (data?.next?.cursor) { + console.log(`[Farcaster] Target usernames [${this.TARGET_USERNAMES.join(', ')}] not found yet, fetching next page...`); + await this.fetchFollowersUntilTarget(userFid, data.next.cursor); + } else { + console.log(`[Farcaster] ⚠️ Reached end of followers list, none of [${this.TARGET_USERNAMES.join(', ')}] found`); + this.result(new Error(`None of the target usernames (${this.TARGET_USERNAMES.join(', ')}) are following this user`)); } + } catch (err) { + console.error('[Farcaster] Error fetching followers:', err); + this.result(err as Error); } + } + + private async onFollowersCaptured(log: Array) { + console.log('[Farcaster] ✅ Followers request captured!', log[0].url); - // Check if we have the followers requests - const followersRequests = this.capturedRequests.filter( - req => req.url.includes('/followers?fid=') - ); + // Check if this request contains one of our target usernames + const followersRequestWithTarget = await this.findFollowersRequestWithTargetUsername(log); - if (onboardingRequest && followersRequests.length > 0) { - // Find which followers request contains one of our target usernames - const followersRequestWithTarget = await this.findFollowersRequestWithTargetUsername(followersRequests); - - if (followersRequestWithTarget) { - // We have both requests, proceed with notarization - await this.processNotarization(onboardingRequest, followersRequestWithTarget); - } + if (followersRequestWithTarget) { + // Found a request with target username - notarize ONLY this one! + console.log('[Farcaster] Found target username, starting notarization'); + await this.notarizeFollowersRequest(followersRequestWithTarget); + } else { + console.log('[Farcaster] Target usernames not in this request, continuing...'); + // Continue fetching - fetchFollowersUntilTarget is handling pagination } } @@ -115,73 +189,23 @@ export class NotarizationFarcasterLegitFollowers extends NotarizationBase { followersRequests: Array ): Promise { // Check each followers request to see which one contains one of our target usernames - for (const request of followersRequests) { - try { - const notary = await TLSNotary.new( - { - serverDns: 'client.farcaster.xyz', - maxSentData: 2048, - maxRecvData: 16384, - }, - { - logEveryNMessages: 100, - verbose: false, - logPrefix: '[WS Monitor / Farcaster-Check-Target]', - trackSize: false, - expectedTotalBytes: 55000000 * 1.15, - enableProgress: false, - progressUpdateInterval: 500, - }, - ); - - const reqCopy = { ...request }; - delete reqCopy.headers['Accept-Encoding']; - - const result = await notary.transcript(reqCopy); - if (!(result instanceof Error)) { - const [, message] = result; - const responseData = JSON.parse(message.body.toString()); - const users = responseData?.result?.users || []; - - // Check if any of our target usernames are in this page - for (const targetUsername of this.TARGET_USERNAMES) { - const foundUser = users.find((user: any) => user.username === targetUsername); - if (foundUser) { - this.foundUsername = targetUsername; - console.log(`[Farcaster] Found the followers request containing ${targetUsername}`); - return request; - } - } - } - } catch (err) { - console.error('[Farcaster] Error checking request for target usernames:', err); - } + // We need to actually notarize to get the response, so we'll just return the first one + // and check during notarization + + // For now, just return the most recent followers request + // The actual check will happen during notarization + if (followersRequests.length > 0) { + const latestRequest = followersRequests[followersRequests.length - 1]; + console.log(`[Farcaster] Using latest followers request: ${latestRequest.url}`); + return latestRequest; } - console.log(`[Farcaster] None of the target usernames (${this.TARGET_USERNAMES.join(', ')}) found in captured followers requests`); + console.log(`[Farcaster] No followers requests captured yet`); return null; } - private async triggerFollowersRequest(fid: number): Promise { - // Send a message to the Farcaster content script to fetch followers - if (this.currentTabId) { - try { - await chrome.tabs.sendMessage(this.currentTabId, { - type: 'FETCH_FARCASTER_FOLLOWERS', - payload: { - fid, - targetUsernames: this.TARGET_USERNAMES, - }, - }); - console.log('[Farcaster] Sent message to content script to fetch followers'); - } catch (err) { - console.error('[Farcaster] Error sending message to content script:', err); - } - } - } - private async processNotarization( - onboardingRequest: Request, + private async notarizeFollowersRequest( followersRequest: Request ): Promise { this.currentStep = 2; @@ -189,10 +213,12 @@ export class NotarizationFarcasterLegitFollowers extends NotarizationBase { this.currentStepUpdateCallback(this.currentStep); try { + console.log('[Farcaster] Starting TLSNotary for followers request'); + const notary = await TLSNotary.new( { serverDns: 'client.farcaster.xyz', - maxSentData: 4096, + maxSentData: 2048, maxRecvData: 16384, }, { @@ -206,76 +232,32 @@ export class NotarizationFarcasterLegitFollowers extends NotarizationBase { }, ); - // Process onboarding-state request for username - delete onboardingRequest.headers['Accept-Encoding']; + // Notarize ONLY the followers request + delete followersRequest.headers['Accept-Encoding']; - const result1 = await notary.transcript(onboardingRequest); - if (result1 instanceof Error) { - this.result(result1); + const result = await notary.transcript(followersRequest); + if (result instanceof Error) { + this.result(result); return; } - const [transcript1, message1] = result1; + const [transcript, message] = result; const commit: Commit = { - sent: [{ start: 0, end: transcript1.sent.length }], - recv: [{ start: 0, end: message1.info.length }], + sent: [{ start: 0, end: transcript.sent.length }], + recv: [{ start: 0, end: message.info.length }], }; - const jsonStarts1: number = Buffer.from(transcript1.recv) + const jsonStarts: number = Buffer.from(transcript.recv) .toString('utf-8') .indexOf('{'); - - const pointers1: Pointers = parse(message1.body.toString()).pointers; - - const username: Mapping = pointers1['/result/state/user/username']; - const fid: Mapping = pointers1['/result/state/user/fid']; - console.log({ pointers: pointers1 }); - if (!username.key?.pos) { - this.result(new Error('username not found')); - return; - } - - if (!fid.key?.pos) { - this.result(new Error('fid not found')); - return; - } - - // Commit username and fid from onboarding-state - commit.recv.push({ - start: jsonStarts1 + username.key?.pos, - end: jsonStarts1 + username.valueEnd.pos, - }); - - commit.recv.push({ - start: jsonStarts1 + fid.key?.pos, - end: jsonStarts1 + fid.valueEnd.pos, - }); - - // Process followers request to check for vitalik.eth - delete followersRequest.headers['Accept-Encoding']; - - const result2 = await notary.transcript(followersRequest); - if (result2 instanceof Error) { - this.result(result2); - return; - } - const [transcript2, message2] = result2; - - const jsonStarts2: number = Buffer.from(transcript2.recv) - .toString('utf-8') - .indexOf('{'); - - const followersData = JSON.parse(message2.body.toString()); - const pointers2: Pointers = parse(message2.body.toString()).pointers; + const followersData = JSON.parse(message.body.toString()); + const pointers: Pointers = parse(message.body.toString()).pointers; // Check if our target username is in the followers list let targetFollowerIndex = -1; const users = followersData?.result?.users || []; - // Use the username we found earlier, or fall back to checking all targets - const usernameToFind = this.foundUsername || this.TARGET_USERNAMES[0]; - for (let i = 0; i < users.length; i++) { if (this.TARGET_USERNAMES.includes(users[i].username)) { targetFollowerIndex = i; @@ -292,8 +274,8 @@ export class NotarizationFarcasterLegitFollowers extends NotarizationBase { console.log(`[Farcaster] Found ${this.foundUsername} at followers index ${targetFollowerIndex}`); // Commit the target follower entry - const targetUsername: Mapping = pointers2[`/result/users/${targetFollowerIndex}/username`]; - const targetFollowedBy: Mapping = pointers2[`/result/users/${targetFollowerIndex}/viewerContext/followedBy`]; + const targetUsername: Mapping = pointers[`/result/users/${targetFollowerIndex}/username`]; + const targetFollowedBy: Mapping = pointers[`/result/users/${targetFollowerIndex}/viewerContext/followedBy`]; if (!targetUsername.key?.pos) { this.result(new Error(`${this.foundUsername} username pointer not found`)); @@ -307,14 +289,14 @@ export class NotarizationFarcasterLegitFollowers extends NotarizationBase { // Commit target username commit.recv.push({ - start: jsonStarts2 + targetUsername.key?.pos, - end: jsonStarts2 + targetUsername.valueEnd.pos, + start: jsonStarts + targetUsername.key?.pos, + end: jsonStarts + targetUsername.valueEnd.pos, }); // Commit followedBy status (should be true) commit.recv.push({ - start: jsonStarts2 + targetFollowedBy.key?.pos, - end: jsonStarts2 + targetFollowedBy.valueEnd.pos, + start: jsonStarts + targetFollowedBy.key?.pos, + end: jsonStarts + targetFollowedBy.valueEnd.pos, }); console.log({ commit }); @@ -327,6 +309,9 @@ export class NotarizationFarcasterLegitFollowers extends NotarizationBase { public async onStop(): Promise { this.requestRecorder.stop(); + if (this.followersRecorder) { + this.followersRecorder.stop(); + } } } diff --git a/webpack.config.js b/webpack.config.js index be2068a..bb35806 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -45,8 +45,7 @@ const options = { // should be injected to webpage contentScript: path.join(__dirname, "src", "content", "index.tsx"), - content: path.join(__dirname, "src", "content", "content.tsx"), - farcasterHelper: path.join(__dirname, "src", "content", "farcaster-helper.tsx") + content: path.join(__dirname, "src", "content", "content.tsx") }, output: { filename: "[name].bundle.js",