feat(farcaster): infer fid on signup; authorize signer only when needed#1735
feat(farcaster): infer fid on signup; authorize signer only when needed#1735Jhonattan2121 wants to merge 11 commits intocanaryfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds wallet-based FID inference (new /api/fid-link/infer), exposes inferFidForCurrentIdentity to app state, makes signer initialization lazy with readiness polling, updates registration to try inference then fallback to Farcaster authenticator flow, and makes fidRegistrations signing fields nullable. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Frontend as LoggedInStateProvider
participant Store as FarcasterStore
participant API as /api/fid-link/infer
participant Neynar as Neynar API
participant DB as Database
User->>Frontend: Start registration (wallet connected)
Frontend->>Store: inferFidForCurrentIdentity(walletAddress)
Store->>Store: Sign InferFidLinkRequest with identity key
Store->>API: POST signed request
API->>API: Verify ED25519 signature
API->>Neynar: Query for FID
Neynar-->>API: Return inferred FID or null
alt FID inferred
API->>DB: Upsert fidRegistrations (case-insensitive wallet match)
DB-->>API: Ack
end
API-->>Store: Return inferred FID (or null)
Store->>Frontend: Update identity or signal fallback
alt FID inferred
Frontend->>Frontend: Complete registration
else
Frontend->>Frontend: Open Farcaster modal and initialize authenticator
end
sequenceDiagram
participant User
participant Component as CastRow/CreateCast
participant Hook as useFarcasterSigner
participant AuthMgr as AuthenticatorManager
participant Authenticator
User->>Component: Trigger action (post/like)
Component->>Hook: getOrCreateSigner()
alt signer available
Hook-->>Component: return signer
else
Hook->>AuthMgr: ensure/wait for authenticator ready
AuthMgr-->>Hook: ready
Hook->>Authenticator: request signer
Authenticator-->>Hook: return signer or null
Hook-->>Component: return signer or null
end
Component->>Component: Proceed or show toast on failure
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/common/providers/LoggedInStateProvider.tsx`:
- Around line 199-219: The code currently silently returns when
waitForAuthenticatorReady("farcaster:nounspace") times out or when
authenticatorManager.callMethod for methodName "getAccountFid" /
"getSignerPublicKey" fails, leaving the UI stuck on
SetupStep.AUTHENTICATORS_INITIALIZED; update the error paths to surface
user-facing feedback and logging (e.g., set an error state/message or call the
existing toast/modal error handler and log the error via processLogger or
console), and optionally offer a retry or explicit cancel action so the modal
can close or retry the authenticator flow rather than returning silently from
the function.
In `@src/pages/api/fid-link/infer.ts`:
- Around line 123-167: The branch that skips an update when the DB record is
newer currently returns the caller's body.identityPublicKey which can
misrepresent ownership; inside the if (existing && existing.length > 0) branch
where you compute currentRecord and check
moment(currentRecord?.created).isAfter(created), replace the response payload to
return the identityPublicKey from currentRecord (or null if absent) and use
currentRecord.created for the created field; also compute the comparison against
server time (e.g., use a serverNow variable from moment() instead of the
client-provided body.timestamp) so you don't trust client clocks when deciding
to skip updates for fidRegistrations/inferredFid.
🧹 Nitpick comments (3)
src/common/data/stores/app/accounts/farcasterStore.ts (1)
64-83: Use thesignSignablehelper for consistency with the rest of the codebase.Extract the signing logic to use
signSignable(unsigned, identityPrivateKey)instead of manually callinged25519.sign()andbytesToHex(). This matches the pattern used inhomebaseTabsStore.tsandspaceStore.tsand centralizes signature generation logic.src/common/providers/LoggedInStateProvider.tsx (2)
74-85: Consider adding cleanup for component unmount.The polling loop will continue running even if the component unmounts (e.g., user navigates away), which could cause state updates on an unmounted component. Since this is inside an async function called from
registerAccounts, you may want to use an abort signal or ref-based flag to break out of the loop.♻️ Optional: Add abort handling
const waitForAuthenticatorReady = async ( authenticatorId: string, timeoutMs = 60_000, + signal?: AbortSignal, ) => { const start = Date.now(); while (Date.now() - start < timeoutMs) { + if (signal?.aborted) return false; const initialized = await authenticatorManager.getInitializedAuthenticators(); if (initialized.includes(authenticatorId)) return true; await new Promise((r) => setTimeout(r, 500)); } return false; };
221-221: Add type annotation tomessageHashparameter.The
signForFidcallback parameter lacks a TypeScript type, which reduces type safety and IDE support.♻️ Proposed fix
- const signForFid = async (messageHash) => { + const signForFid = async (messageHash: Uint8Array) => {
src/pages/api/fid-link/infer.ts
Outdated
| const created = body.timestamp || moment().toISOString(); | ||
|
|
||
| // Upsert by fid (unique), but allow signer-related fields to remain null. | ||
| const { data: existing } = await supabase | ||
| .from("fidRegistrations") | ||
| .select("fid, created") | ||
| .eq("fid", inferredFid); | ||
|
|
||
| if (existing && existing.length > 0) { | ||
| const currentRecord = first(existing); | ||
| if (moment(currentRecord?.created).isAfter(created)) { | ||
| return res.status(200).json({ | ||
| result: "success", | ||
| value: { | ||
| fid: inferredFid, | ||
| identityPublicKey: body.identityPublicKey, | ||
| created: currentRecord!.created, | ||
| inferredFromAddress: walletAddress, | ||
| }, | ||
| }); | ||
| } | ||
| const { data, error } = await supabase | ||
| .from("fidRegistrations") | ||
| .update({ | ||
| created, | ||
| identityPublicKey: body.identityPublicKey, | ||
| isSigningKeyValid: false, | ||
| signature: null, | ||
| signingKeyLastValidatedAt: null, | ||
| signingPublicKey: null, | ||
| }) | ||
| .eq("fid", inferredFid) | ||
| .select(); | ||
| if (error) { | ||
| return res.status(500).json({ result: "error", error: { message: error.message } }); | ||
| } | ||
| return res.status(200).json({ | ||
| result: "success", | ||
| value: { | ||
| fid: inferredFid, | ||
| identityPublicKey: body.identityPublicKey, | ||
| created, | ||
| inferredFromAddress: walletAddress, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Avoid returning a “linked” response when no update occurred.
If existing.created is newer than the request timestamp, you skip the update but still return the caller’s identityPublicKey. That can cause the client to attach a FID it doesn’t actually own (especially with skewed client clocks). Return the existing record’s identity (or null) in that branch. Consider using server time for comparisons to avoid skew.
✅ Suggested fix
- const { data: existing } = await supabase
- .from("fidRegistrations")
- .select("fid, created")
+ const { data: existing } = await supabase
+ .from("fidRegistrations")
+ .select("fid, created, identityPublicKey")
.eq("fid", inferredFid);
if (existing && existing.length > 0) {
const currentRecord = first(existing);
if (moment(currentRecord?.created).isAfter(created)) {
return res.status(200).json({
result: "success",
value: {
fid: inferredFid,
- identityPublicKey: body.identityPublicKey,
- created: currentRecord!.created,
+ identityPublicKey: currentRecord!.identityPublicKey,
+ created: currentRecord!.created,
inferredFromAddress: walletAddress,
},
});
}🤖 Prompt for AI Agents
In `@src/pages/api/fid-link/infer.ts` around lines 123 - 167, The branch that
skips an update when the DB record is newer currently returns the caller's
body.identityPublicKey which can misrepresent ownership; inside the if (existing
&& existing.length > 0) branch where you compute currentRecord and check
moment(currentRecord?.created).isAfter(created), replace the response payload to
return the identityPublicKey from currentRecord (or null if absent) and use
currentRecord.created for the created field; also compute the comparison against
server time (e.g., use a serverNow variable from moment() instead of the
client-provided body.timestamp) so you don't trust client clocks when deciding
to skip updates for fidRegistrations/inferredFid.
There was a problem hiding this comment.
Pull request overview
This PR modifies the Farcaster integration to defer signer authorization until users perform write actions, while enabling FID inference from wallet addresses during signup.
Changes:
- Made Farcaster signer fields nullable in the database to support FID inference without immediate signer authorization
- Added a new
/api/fid-link/inferendpoint to infer a user's FID from their wallet address via Neynar - Modified UI components to request signer authorization on-demand when users perform write actions (cast, like, follow, etc.)
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
supabase/migrations/20260129000000_make_fid_registration_signer_optional.sql |
Makes signer-related columns nullable in fidRegistrations table |
src/supabase/database.d.ts |
Updates TypeScript types to reflect nullable signer fields |
src/pages/api/fid-link/infer.ts |
New endpoint to infer FID from wallet address and create registration without signer |
src/pages/api/fid-link.ts |
Removes isSigningKeyValid filter from FID lookup endpoint |
src/pages/api/space/identities.ts |
Updates wallet address lookup to use case-insensitive matching |
src/fidgets/ui/profile.tsx |
Updates follow/unfollow to request signer on-demand |
src/fidgets/farcaster/index.tsx |
Adds ensureSigner and getOrCreateSigner functions to hook |
src/fidgets/farcaster/components/CreateCast.tsx |
Updates cast creation to request signer on-demand |
src/fidgets/farcaster/components/CastRow.tsx |
Updates reactions to request signer on-demand |
src/constants/requiredAuthenticators.ts |
Removes Farcaster from required authenticators list |
src/common/providers/LoggedInStateProvider.tsx |
Implements FID inference in signup flow with Farcaster fallback |
src/common/lib/hooks/useCurrentFid.ts |
Prioritizes inferred FID from associatedFids over authenticator FID |
src/common/data/stores/app/accounts/farcasterStore.ts |
Adds inferFidForCurrentIdentity function with analytics tracking |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/pages/api/fid-link/infer.ts
Outdated
| const { data, error } = await supabase | ||
| .from("fidRegistrations") | ||
| .update({ | ||
| created, | ||
| identityPublicKey: body.identityPublicKey, | ||
| isSigningKeyValid: false, | ||
| signature: null, | ||
| signingKeyLastValidatedAt: null, | ||
| signingPublicKey: null, | ||
| }) | ||
| .eq("fid", inferredFid) | ||
| .select(); |
There was a problem hiding this comment.
The identityPublicKey field should also be validated to ensure we're not overwriting an existing record that belongs to a different identity. Consider adding a check: if the existing record has a different identityPublicKey than the one in the request, return an error rather than silently updating it.
src/pages/api/fid-link/infer.ts
Outdated
| isSigningKeyValid: false, | ||
| signature: null, | ||
| signingKeyLastValidatedAt: null, | ||
| signingPublicKey: null, |
There was a problem hiding this comment.
When updating an existing FID registration, the code unconditionally sets all signer-related fields to null and isSigningKeyValid to false. This will overwrite any existing valid signer that was previously authorized. The infer endpoint should only create new records or update the identityPublicKey linkage; it should not clear out existing signer data that may have been set through the regular registration flow.
| isSigningKeyValid: false, | |
| signature: null, | |
| signingKeyLastValidatedAt: null, | |
| signingPublicKey: null, |
src/pages/api/fid-link/infer.ts
Outdated
| const { data: existing } = await supabase | ||
| .from("fidRegistrations") | ||
| .select("fid, created") | ||
| .eq("fid", inferredFid); | ||
|
|
||
| if (existing && existing.length > 0) { | ||
| const currentRecord = first(existing); | ||
| if (moment(currentRecord?.created).isAfter(created)) { | ||
| return res.status(200).json({ | ||
| result: "success", | ||
| value: { | ||
| fid: inferredFid, | ||
| identityPublicKey: body.identityPublicKey, | ||
| created: currentRecord!.created, | ||
| inferredFromAddress: walletAddress, | ||
| }, | ||
| }); | ||
| } | ||
| const { data, error } = await supabase | ||
| .from("fidRegistrations") | ||
| .update({ | ||
| created, | ||
| identityPublicKey: body.identityPublicKey, | ||
| isSigningKeyValid: false, | ||
| signature: null, | ||
| signingKeyLastValidatedAt: null, | ||
| signingPublicKey: null, | ||
| }) | ||
| .eq("fid", inferredFid) | ||
| .select(); | ||
| if (error) { | ||
| return res.status(500).json({ result: "error", error: { message: error.message } }); | ||
| } | ||
| return res.status(200).json({ | ||
| result: "success", | ||
| value: { | ||
| fid: inferredFid, | ||
| identityPublicKey: body.identityPublicKey, | ||
| created, | ||
| inferredFromAddress: walletAddress, | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
There's a potential race condition between checking for existing records and updating them. If two requests with the same FID arrive simultaneously, both might pass the existence check and attempt to update or insert, potentially causing unexpected behavior. Consider using an upsert operation with proper conflict resolution instead of separate select and update/insert operations.
| // Fallback: if we still can't infer an FID, prompt the user to connect to Farcaster. | ||
| await authenticatorManager.installAuthenticators(["farcaster:nounspace"]); | ||
| authenticatorManager.initializeAuthenticators(["farcaster:nounspace"]); | ||
| setModalOpen(true); | ||
|
|
||
| const ready = await waitForAuthenticatorReady("farcaster:nounspace"); | ||
| if (!ready) { | ||
| // Keep the user on this step until they finish the Farcaster flow. | ||
| return; | ||
| } | ||
|
|
||
| const fidResult = await authenticatorManager.callMethod({ | ||
| requestingFidgetId: "root", | ||
| authenticatorId: "farcaster:nounspace", | ||
| methodName: "getAccountFid", | ||
| isLookup: true, | ||
| }); | ||
| if (fidResult.result !== "success") return; | ||
|
|
||
| const publicKeyResult = await authenticatorManager.callMethod({ | ||
| requestingFidgetId: "root", | ||
| authenticatorId: "farcaster:nounspace", | ||
| methodName: "getSignerPublicKey", | ||
| isLookup: true, | ||
| }); | ||
| if (publicKeyResult.result !== "success") return; | ||
|
|
||
| const signForFid = async (messageHash) => { | ||
| const signResult = await authenticatorManager.callMethod( | ||
| { | ||
| requestingFidgetId: "root", | ||
| authenticatorId: "farcaster:nounspace", | ||
| methodName: "signMessage", | ||
| isLookup: false, | ||
| }, | ||
| messageHash, | ||
| ); | ||
| if (signResult.result !== "success") { | ||
| throw new Error("Failed to sign message"); | ||
| } | ||
| return signResult.value as Uint8Array; | ||
| }; | ||
|
|
||
| await registerFidForCurrentIdentity( | ||
| fidResult.value as number, | ||
| bytesToHex(publicKeyResult.value as Uint8Array), | ||
| signForFid, | ||
| ); | ||
|
|
||
| setCurrentStep(SetupStep.ACCOUNTS_REGISTERED); |
There was a problem hiding this comment.
The error handling for this fallback flow could lead to a poor user experience. If the Farcaster authenticator fails to initialize or if any of the method calls fail, the function returns early without setting the step to ACCOUNTS_REGISTERED. This could leave the user stuck in the registration flow. Consider adding more specific error handling or a retry mechanism to help users recover from transient failures.
|
|
||
| export const useCurrentFid = (): number | null => { | ||
| return useAppStore((state) => { | ||
| const currentIdentity = state.account.getCurrentIdentity?.(); |
There was a problem hiding this comment.
The hook always returns the first FID from associatedFids array without considering whether the user might have multiple FIDs. If a user has multiple FIDs associated with their identity, this will always return the first one, which may not be the one they want to use. Consider whether there should be a way for users to select which FID they want to use, or document that only the first FID is used.
| const currentIdentity = state.account.getCurrentIdentity?.(); | |
| const currentIdentity = state.account.getCurrentIdentity?.(); | |
| // NOTE: When multiple FIDs are associated with the current identity, | |
| // this hook intentionally uses the first FID in `associatedFids` as the | |
| // "current" FID. Callers that need to let users choose a different FID | |
| // should implement that selection logic separately. |
| if (existing && existing.length > 0) { | ||
| const currentRecord = first(existing); | ||
| if (moment(currentRecord?.created).isAfter(created)) { | ||
| return res.status(200).json({ | ||
| result: "success", | ||
| value: { | ||
| fid: inferredFid, | ||
| identityPublicKey: body.identityPublicKey, | ||
| created: currentRecord!.created, | ||
| inferredFromAddress: walletAddress, | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
The timestamp comparison logic is inverted. When a current record has a more recent 'created' timestamp, the function returns early with the existing data. However, if the existing record is older, the code proceeds to update it with null signer fields. This means that if a user previously authorized a signer (with valid signature/signingPublicKey), and then calls this endpoint again with an older timestamp, it would wipe out their valid signer data. The condition should prevent updates when the new timestamp is older, not when the current timestamp is newer.
| const ensureSigner = async () => { | ||
| if (!isAuthenticatorInstalled) { | ||
| await authenticatorManager.installAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); | ||
| authenticatorManager.initializeAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); | ||
| } else { | ||
| authenticatorManager.initializeAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); | ||
| } | ||
| setModalOpen(true); | ||
|
|
There was a problem hiding this comment.
The ensureSigner function has a potential issue: if the authenticator is already installed but not initialized, it will call initializeAuthenticators and then immediately open the modal. However, if the authenticator is not installed, it will install and then initialize before opening the modal. Consider whether there should be consistent behavior in both cases, or if the modal should only open when the user needs to take action.
src/fidgets/farcaster/index.tsx
Outdated
| const start = Date.now(); | ||
| const timeoutMs = 60_000; | ||
| setIsLoadingSigner(true); | ||
| try { | ||
| while (Date.now() - start < timeoutMs) { | ||
| const initialized = await authenticatorManager.getInitializedAuthenticators(); | ||
| if (indexOf(initialized, FARCASTER_AUTHENTICATOR_NAME) !== -1) { | ||
| setHasSigner(true); | ||
| return true; | ||
| } | ||
| await new Promise((r) => setTimeout(r, 500)); | ||
| } | ||
| return false; | ||
| } finally { | ||
| setIsLoadingSigner(false); | ||
| } |
There was a problem hiding this comment.
There is code duplication between the ensureSigner function here and the waitForAuthenticatorReady function in LoggedInStateProvider. Both implement the same polling logic to wait for an authenticator to be ready. Consider extracting this into a shared utility function to improve maintainability and ensure consistency across the codebase.
src/pages/api/fid-link/infer.ts
Outdated
| }, | ||
| }); | ||
| } | ||
| const { data, error } = await supabase |
There was a problem hiding this comment.
Unused variable data.
| const { data, error } = await supabase | |
| const { error } = await supabase |
| -- Allow linking an identity to an inferred FID without requiring a Farcaster signer. | ||
| -- Signer-related fields remain nullable until the user authorizes Nounspace as a signer. | ||
|
|
||
| alter table public."fidRegistrations" | ||
| alter column "signature" drop not null, | ||
| alter column "signingPublicKey" drop not null, | ||
| alter column "signingKeyLastValidatedAt" drop not null; | ||
|
|
||
| alter table public."fidRegistrations" | ||
| alter column "isSigningKeyValid" set default false; | ||
|
|
There was a problem hiding this comment.
This migration drops the NOT NULL constraint on signature, signingPublicKey, and signingKeyLastValidatedAt and sets isSigningKeyValid to default false, allowing fidRegistrations rows to exist for a FID without any verified Farcaster signer. Because existing authorization logic (e.g., identityCanRegisterForFid, space registry, and space ordering) already treats the mere presence of a fidRegistrations row for an identityPublicKey/fid pair as proof of control—and identities can obtain such rows via the FID inference flow that trusts forgeable walletIdentities bindings—an attacker can link their identity to a victim’s FID and gain unauthorized management of that FID’s spaces. To mitigate this, ensure that any access control based on fidRegistrations also enforces a trusted signer (e.g., require isSigningKeyValid = true and non-null signer fields) or segregate "inferred but unauthenticated" FID links into a separate state/table that is never used for authorization decisions.
| -- Allow linking an identity to an inferred FID without requiring a Farcaster signer. | |
| -- Signer-related fields remain nullable until the user authorizes Nounspace as a signer. | |
| alter table public."fidRegistrations" | |
| alter column "signature" drop not null, | |
| alter column "signingPublicKey" drop not null, | |
| alter column "signingKeyLastValidatedAt" drop not null; | |
| alter table public."fidRegistrations" | |
| alter column "isSigningKeyValid" set default false; | |
| -- NOTE: This migration intentionally does NOT relax signer constraints on "fidRegistrations". | |
| -- | |
| -- Background: | |
| -- Dropping NOT NULL on "signature", "signingPublicKey", and "signingKeyLastValidatedAt" | |
| -- and setting "isSigningKeyValid" to default FALSE would allow "fidRegistrations" rows | |
| -- to exist for a FID without any verified Farcaster signer. Existing authorization logic | |
| -- treats the presence of a "fidRegistrations" row for an ("identityPublicKey", "fid") | |
| -- pair as proof of control, so such a change would enable linking an attacker-controlled | |
| -- identity to a victim’s FID and gaining unauthorized management of that FID’s spaces. | |
| -- | |
| -- Mitigation: | |
| -- - Keep signer-related fields and "isSigningKeyValid" semantics unchanged for the | |
| -- "fidRegistrations" table, so that only fully authenticated links are represented. | |
| -- - If inferred-but-unauthenticated FID links are needed, they must be stored in a | |
| -- separate construct that is never used for authorization decisions. | |
| -- | |
| -- This migration is therefore a no-op to avoid weakening the authorization primitive. |
…n queue management
… required for signup
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/authenticators/AuthenticatorManager.tsx (1)
302-305:⚠️ Potential issue | 🟡 MinorInconsistent state update pattern may cause stale closure bugs.
completeInstallingCurrentInitializerusestail(initializationQueue)directly, which references theinitializationQueuevalue captured at render time. If the queue was modified by concurrentinitializeAuthenticatorscalls, this could skip items.Use a functional updater for consistency with the other state updates:
🔧 Suggested fix
function completeInstallingCurrentInitializer() { - setInitializationQueue(tail(initializationQueue)); + setInitializationQueue((queue) => tail(queue)); setCurrentInitializer(undefined); }supabase/migrations/20240614000356_setup_db.sql (1)
34-43:⚠️ Potential issue | 🟠 MajorCreate a new migration file instead of modifying the existing one.
This migration file was created in June 2024 and has already been deployed. Modifying it in place won't apply changes to databases where it's already run.
Create a new migration file (e.g.,
20260129000000_make_fid_signer_fields_nullable.sql) with the followingALTER TABLEstatements:Suggested migration
ALTER TABLE "public"."fidRegistrations" ALTER COLUMN "signature" DROP NOT NULL, ALTER COLUMN "signingPublicKey" DROP NOT NULL, ALTER COLUMN "signingKeyLastValidatedAt" DROP NOT NULL, ALTER COLUMN "isSigningKeyValid" SET DEFAULT false;
🤖 Fix all issues with AI agents
In `@src/fidgets/farcaster/components/CreateCast.tsx`:
- Around line 1002-1005: The submit flow allows concurrent signer creation and
publishing; update the logic in CreateCast so the Button (rendered with
getButtonText) is disabled when isLoadingSigner OR isPublishing, and add guards
around getOrCreateSigner and publishPost to prevent re-entry: set draft.status =
DraftStatus.publishing (or an explicit inFlight boolean) immediately before
awaiting publishPost(), ensure publishPost and getOrCreateSigner check and set
this flag (or DraftStatus) to short-circuit duplicate calls, and clear the
flag/status on both success and all error paths so users cannot trigger
concurrent submissions.
In `@src/fidgets/farcaster/index.tsx`:
- Around line 97-101: The local variable name "initilizedAuths" inside the
useEffect callback is misspelled; rename it to "initializedAuths" where it is
declared in the promise resolution from
authenticatorManager.getInitializedAuthenticators() and update its usage in the
setHasSigner call (the code inside useEffect that references initilizedAuths and
FARCASTER_AUTHENTICATOR_NAME should use initializedAuths instead).
🧹 Nitpick comments (3)
src/common/data/stores/app/accounts/farcasterStore.ts (1)
10-10: Unused import:hashObject
hashObjectis imported but not used in this file. It's only used internally bysignSignable.🧹 Suggested fix
-import { signSignable , hashObject} from "@/common/lib/signedFiles"; +import { signSignable } from "@/common/lib/signedFiles";src/common/lib/authenticators/waitForAuthenticatorReady.ts (1)
25-37: Consider adding cancellation support for the polling loop.The polling implementation is correct, but there's no way to cancel it once started. If the user navigates away or the calling component unmounts while waiting, the loop continues until timeout (up to 60 seconds), potentially causing state updates on unmounted components.
♻️ Optional: Add AbortSignal support
export async function waitForAuthenticatorReady( authenticatorManager: AuthenticatorManager, authenticatorId: string, timeoutMs = 60_000, + signal?: AbortSignal, ): Promise<boolean> { const start = Date.now(); while (Date.now() - start < timeoutMs) { + if (signal?.aborted) return false; const initialized = await authenticatorManager.getInitializedAuthenticators(); if (initialized.includes(authenticatorId)) return true; await new Promise((r) => setTimeout(r, 500)); } return false; }src/fidgets/farcaster/index.tsx (1)
66-86: Simplify: hoist commoninitializeAuthenticatorscall.The
initializeAuthenticatorscall is identical in both branches and can be moved outside the conditional.♻️ Simplify the branching
const ensureSigner = async () => { if (!isAuthenticatorInstalled) { await authenticatorManager.installAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); - authenticatorManager.initializeAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); - } else { - authenticatorManager.initializeAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); } + authenticatorManager.initializeAuthenticators([FARCASTER_AUTHENTICATOR_NAME]); setModalOpen(true);
| disabled={isPublishing} | ||
| > | ||
| {getButtonText()} | ||
| </Button> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the implementation of ensureSigner and getOrCreateSigner
rg -n -A15 "ensureSigner|getOrCreateSigner" src/fidgets/farcaster/index.tsx | head -100Repository: blankdotspace/space-system
Length of output: 1566
🏁 Script executed:
#!/bin/bash
# Also check if there's any loading state or pending flag to prevent concurrent calls
rg -n "hasSigner|isLoadingSigner" src/fidgets/farcaster/components/CreateCast.tsx | head -30Repository: blankdotspace/space-system
Length of output: 340
🏁 Script executed:
#!/bin/bash
# Look for the full hook implementation to understand state management
fd "\.tsx$|\.ts$" src/fidgets/farcaster/ -type f | head -20Repository: blankdotspace/space-system
Length of output: 240
🏁 Script executed:
#!/bin/bash
# Find where getOrCreateSigner is called in CreateCast.tsx
rg -n "getOrCreateSigner" src/fidgets/farcaster/components/CreateCast.tsx -B3 -A3Repository: blankdotspace/space-system
Length of output: 783
🏁 Script executed:
#!/bin/bash
# Check the complete onClick handler logic
rg -n "onClick.*{" src/fidgets/farcaster/components/CreateCast.tsx | head -20Repository: blankdotspace/space-system
Length of output: 153
🏁 Script executed:
#!/bin/bash
# Look for the submit/click handler that uses getOrCreateSigner
rg -n -A20 "handlePublish|handleSubmit|onClick" src/fidgets/farcaster/components/CreateCast.tsx | grep -A20 "getOrCreateSigner"Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Find onSubmitPost usage and button onClick handlers
rg -n "onSubmitPost\|onClick" src/fidgets/farcaster/components/CreateCast.tsx -A2 -B2 | head -60Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look for isPublishing state management and button onClick
rg -n "isPublishing\|setIsPublishing" src/fidgets/farcaster/components/CreateCast.tsx | head -20Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Get the context around line 1002 where the button is
sed -n '995,1010p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 570
🏁 Script executed:
#!/bin/bash
# Look for the complete button definition with onClick
sed -n '970,1010p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 1686
🏁 Script executed:
#!/bin/bash
# Search for all instances of isPublishing
rg -n "isPublishing" src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 659
🏁 Script executed:
#!/bin/bash
# Search for onSubmitPost and the button that calls it
rg -n "onSubmitPost\|onClick.*=>" src/fidgets/farcaster/components/CreateCast.tsx -B2 -A5Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Find the form element and onSubmit handler
rg -n "form\|onSubmit" src/fidgets/farcaster/components/CreateCast.tsx -i | head -30Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look at the complete onSubmitPost function
sed -n '415,450p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 1239
🏁 Script executed:
#!/bin/bash
# Check if there's any state management that prevents concurrent calls to onSubmitPost
sed -n '460,480p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 589
🏁 Script executed:
#!/bin/bash
# Find the form element with onSubmit handler
rg -n "<form\|onSubmit" src/fidgets/farcaster/components/CreateCast.tsx -B2 -A8Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Check how draft.status is updated to publishing
rg -n "DraftStatus.publishing\|setDraft.*publishing" src/fidgets/farcaster/components/CreateCast.tsx -B2 -A2Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look at the publishPost function to see where draft status is updated
rg -n "publishPost\|DraftStatus" src/fidgets/farcaster/components/CreateCast.tsx | head -40Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Find where draft.status is set to publishing
rg -n "status.*publishing\|publishing.*status" src/fidgets/farcaster/components/CreateCast.tsx -B3 -A3Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look for the form and how onSubmitPost is connected
sed -n '200,250p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 1194
🏁 Script executed:
#!/bin/bash
# Search in entire farcaster directory for draft status updates
rg -n "DraftStatus.publishing" src/fidgets/farcaster/ -B2 -A2Repository: blankdotspace/space-system
Length of output: 515
🏁 Script executed:
#!/bin/bash
# Search for form element more broadly
rg -n "type=\"submit\"" src/fidgets/farcaster/components/CreateCast.tsx -B30 | grep -E "form|onSubmit|handleSubmit" | head -20Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look for where publishPost is called and how it relates to draft status
sed -n '415,465p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 1724
🏁 Script executed:
#!/bin/bash
# Search for all setDraft calls to see where status changes
rg -n "setDraft" src/fidgets/farcaster/components/CreateCast.tsx | head -20Repository: blankdotspace/space-system
Length of output: 387
🏁 Script executed:
#!/bin/bash
# Find all references to DraftStatus.publishing to understand the flow
rg -n "DraftStatus.publishing\|status.*DraftStatus\|publishing" src/fidgets/farcaster/ -B2 -A2 | head -80Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Check the publishPost function to see if it sets draft status
fd -e ts -e tsx src/fidgets/farcaster/ -not -path "*/node_modules/*" | xargs grep -l "publishPost" | head -5Repository: blankdotspace/space-system
Length of output: 300
🏁 Script executed:
#!/bin/bash
# Look at the useFarcasterDraft hook to see complete draft state management
rg -n "useFarcasterDraft\|DraftStatus" src/fidgets/farcaster/ --type tsx --type ts | head -50Repository: blankdotspace/space-system
Length of output: 97
🏁 Script executed:
#!/bin/bash
# Search for DraftStatus.publishing across the entire farcaster directory
rg "DraftStatus.publishing" src/fidgets/farcaster/Repository: blankdotspace/space-system
Length of output: 182
🏁 Script executed:
#!/bin/bash
# Search for publishPost definition
rg -n "const publishPost\|function publishPost" src/fidgets/farcaster/Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look at broader context around onSubmitPost to find the form/button that calls it
sed -n '1,100p' src/fidgets/farcaster/components/CreateCast.tsx | tail -50Repository: blankdotspace/space-system
Length of output: 1477
🏁 Script executed:
#!/bin/bash
# Check if there's a form element that prevents double submission
rg -n "form\|onSubmit\|handleSubmit" src/fidgets/farcaster/components/CreateCast.tsx | head -30Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look for where draft status might be set to publishing or any state guard
rg -n "publishing\|status" src/fidgets/farcaster/components/CreateCast.tsx | grep -E "setDraft|status" | head -30Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Check if submitCast function sets draft status
rg -n "submitCast\|const submitCast" src/fidgets/farcaster/ -A5 -B2 | head -60Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Check the complete CreateCast.tsx file size
wc -l src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 122
🏁 Script executed:
#!/bin/bash
# Search more broadly for button type submit and the handler
rg "type=.submit" src/fidgets/farcaster/components/CreateCast.tsx -B50 | grep -E "form|onSubmit|onClick|handler" | head -20Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look at the parent form context - search for form opening tag
sed -n '850,950p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 3735
🏁 Script executed:
#!/bin/bash
# Search for where draft.status gets set to publishing state
rg -n "DraftStatus.publishing\|status: DraftStatus.publishing\|status.*publishing" src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look at the entire flow - check if there's a form element with onSubmit handler
sed -n '500,600p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 3295
🏁 Script executed:
#!/bin/bash
# Check if there's middleware that wraps onSubmitPost
rg -n "onSubmitPost" src/fidgets/farcaster/components/CreateCast.tsx -B5 -A10Repository: blankdotspace/space-system
Length of output: 1201
🏁 Script executed:
#!/bin/bash
# Check the useEditor hook to understand when onSubmit is called
rg -n "useEditor" src/fidgets/farcaster/components/CreateCast.tsx -A20 | head -50Repository: blankdotspace/space-system
Length of output: 1875
🏁 Script executed:
#!/bin/bash
# Find where draft status is set to publishing - check if there's any state update before calling onSubmitPost
rg -n "status.*publishing\|publishing.*status\|setDraft.*status\|publishing.*setDraft" src/fidgets/farcaster/components/CreateCast.tsx -B3 -A3Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look for where the editor's handleSubmit is called
sed -n '700,800p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 2677
🏁 Script executed:
#!/bin/bash
# Search for useFarcasterDraft hook to understand complete draft state management
rg -n "useFarcasterDraft\|function useFarcasterDraft" src/fidgets/farcaster/ -A20 | head -80Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Look for where draft gets initialized and status management
sed -n '175,190p' src/fidgets/farcaster/components/CreateCast.tsxRepository: blankdotspace/space-system
Length of output: 710
🏁 Script executed:
#!/bin/bash
# Search more broadly for any state updates setting publishing status in entire farcaster directory
rg "publishing\|DraftStatus" src/fidgets/farcaster/ | grep -v "node_modules"Repository: blankdotspace/space-system
Length of output: 52
🏁 Script executed:
#!/bin/bash
# Check if draft.status is ever set to DraftStatus.publishing anywhere
rg "DraftStatus.publishing" src/fidgets/farcaster/Repository: blankdotspace/space-system
Length of output: 182
🏁 Script executed:
#!/bin/bash
# Look at all DraftStatus values to understand the complete enum
rg -n "DraftStatus\." src/fidgets/farcaster/components/CreateCast.tsx | head -20Repository: blankdotspace/space-system
Length of output: 566
🏁 Script executed:
#!/bin/bash
# Search for DraftStatus definition
rg -n "enum DraftStatus\|type DraftStatus\|const DraftStatus" src/fidgets/farcaster/ -A10Repository: blankdotspace/space-system
Length of output: 52
Add submit guard during both signer creation and publishing.
The button lacks protection against concurrent submissions. isPublishing is always false because DraftStatus.publishing is never set in the code—it only transitions between writing and published. Additionally, the button remains clickable during isLoadingSigner, allowing users to trigger getOrCreateSigner multiple times before completion. This creates two concurrent submission risks:
- Multiple simultaneous signer creation calls while
hasSigneris false - Multiple simultaneous publish calls during the entire async flow
Consider setting draft.status = DraftStatus.publishing before awaiting publishPost() and disabling the button accordingly, or add an in-flight flag to prevent re-submission.
🤖 Prompt for AI Agents
In `@src/fidgets/farcaster/components/CreateCast.tsx` around lines 1002 - 1005,
The submit flow allows concurrent signer creation and publishing; update the
logic in CreateCast so the Button (rendered with getButtonText) is disabled when
isLoadingSigner OR isPublishing, and add guards around getOrCreateSigner and
publishPost to prevent re-entry: set draft.status = DraftStatus.publishing (or
an explicit inFlight boolean) immediately before awaiting publishPost(), ensure
publishPost and getOrCreateSigner check and set this flag (or DraftStatus) to
short-circuit duplicate calls, and clear the flag/status on both success and all
error paths so users cannot trigger concurrent submissions.
| useEffect(() => { | ||
| authenticatorManager | ||
| .getInitializedAuthenticators() | ||
| .then((initilizedAuths) => | ||
| setIsLoadingSigner( | ||
| indexOf(initilizedAuths, FARCASTER_AUTHENTICATOR_NAME) === -1, | ||
| ), | ||
| ); | ||
| .then((initilizedAuths) => setHasSigner(indexOf(initilizedAuths, FARCASTER_AUTHENTICATOR_NAME) !== -1)); | ||
| }, [authenticatorManager.lastUpdatedAt]); |
There was a problem hiding this comment.
Fix typo: initilizedAuths → initializedAuths.
Minor typo in the variable name.
📝 Proposed fix
useEffect(() => {
authenticatorManager
.getInitializedAuthenticators()
- .then((initilizedAuths) => setHasSigner(indexOf(initilizedAuths, FARCASTER_AUTHENTICATOR_NAME) !== -1));
+ .then((initializedAuths) => setHasSigner(indexOf(initializedAuths, FARCASTER_AUTHENTICATOR_NAME) !== -1));
}, [authenticatorManager.lastUpdatedAt]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| authenticatorManager | |
| .getInitializedAuthenticators() | |
| .then((initilizedAuths) => | |
| setIsLoadingSigner( | |
| indexOf(initilizedAuths, FARCASTER_AUTHENTICATOR_NAME) === -1, | |
| ), | |
| ); | |
| .then((initilizedAuths) => setHasSigner(indexOf(initilizedAuths, FARCASTER_AUTHENTICATOR_NAME) !== -1)); | |
| }, [authenticatorManager.lastUpdatedAt]); | |
| useEffect(() => { | |
| authenticatorManager | |
| .getInitializedAuthenticators() | |
| .then((initializedAuths) => setHasSigner(indexOf(initializedAuths, FARCASTER_AUTHENTICATOR_NAME) !== -1)); | |
| }, [authenticatorManager.lastUpdatedAt]); |
🤖 Prompt for AI Agents
In `@src/fidgets/farcaster/index.tsx` around lines 97 - 101, The local variable
name "initilizedAuths" inside the useEffect callback is misspelled; rename it to
"initializedAuths" where it is declared in the promise resolution from
authenticatorManager.getInitializedAuthenticators() and update its usage in the
setHasSigner call (the code inside useEffect that references initilizedAuths and
FARCASTER_AUTHENTICATOR_NAME should use initializedAuths instead).
…and adjust profile editability checks.
…e yarn install state.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/app/`(spaces)/s/[handle]/ProfileSpace.tsx:
- Line 30: The import line in ProfileSpace.tsx has a stray space before the
comma in "import React ,{ useMemo } from \"react\""; update the import to the
correct formatting by removing the extra space so it reads "React, { useMemo }"
(locate the import statement at the top of ProfileSpace.tsx). Ensure spacing
around the comma matches other imports in the repo.
🧹 Nitpick comments (1)
src/app/(spaces)/s/[handle]/ProfileSpace.tsx (1)
46-49: Remove or document commented-out code.Commented-out code should either be removed entirely or have a clear comment explaining why it's preserved. If this check is intentionally disabled for the lazy signer flow, remove it. If it might be re-enabled later, add a TODO with context.
♻️ Option 1: Remove commented code (preferred)
- // Require user to be logged in (have an identity key) - // if (!currentUserIdentityPublicKey) { - // console.log('[ProfileSpace] User not logged in - not editable'); - // return false; - // }♻️ Option 2: Add explanatory comment if keeping
- // Require user to be logged in (have an identity key) - // if (!currentUserIdentityPublicKey) { - // console.log('[ProfileSpace] User not logged in - not editable'); - // return false; - // } + // NOTE: Login check removed to support FID-inferred ownership before signer authorization. + // FID ownership check below still validates currentUserFid is defined.
| import { ProfileSpacePageData } from "@/common/types/spaceData"; | ||
| import { useCurrentSpaceIdentityPublicKey } from "@/common/lib/hooks/useCurrentSpaceIdentityPublicKey"; | ||
| import { ProfileSpacePageData } from "@/common/types/spaceData"; | ||
| import React ,{ useMemo } from "react"; |
There was a problem hiding this comment.
Fix formatting in import statement.
There's an extra space before the comma in the React import.
🔧 Proposed fix
-import React ,{ useMemo } from "react";
+import React, { useMemo } from "react";📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import React ,{ useMemo } from "react"; | |
| import React, { useMemo } from "react"; |
🤖 Prompt for AI Agents
In `@src/app/`(spaces)/s/[handle]/ProfileSpace.tsx at line 30, The import line in
ProfileSpace.tsx has a stray space before the comma in "import React ,{ useMemo
} from \"react\""; update the import to the correct formatting by removing the
extra space so it reads "React, { useMemo }" (locate the import statement at the
top of ProfileSpace.tsx). Ensure spacing around the comma matches other imports
in the repo.
What changed
fetchBulkUsersByEthOrSolAddress).identityPublicKey↔fidinfidRegistrationswhile leaving signer fields (signingPublicKey,signature,signingKeyLastValidatedAt) null and settingisSigningKeyValid=false.Summary by CodeRabbit
New Features
Improvements
Chores