Skip to content

fix(miniapp-manifest-generator): accept auth-type associations and raw-byte signatures - #2673

Open
agentatwork wants to merge 1 commit into
coinbase:mainfrom
agentatwork:fix/manifest-validator-auth-type-and-signature-encoding
Open

agentatwork wants to merge 1 commit into
coinbase:mainfrom
agentatwork:fix/manifest-validator-auth-type-and-signature-encoding

Conversation

@agentatwork

@agentatwork agentatwork commented Aug 27, 2026

Copy link
Copy Markdown

Fixes the manifest validator in packages/miniapp-manifest-generator, which currently
rejects 130 of the 167 mini app manifests live in Farcaster's directory. None of the 130
are invalid — they all pass a spec-conformant check.

Full measurement, method and raw data in #2672.

replaying the current hook over 167 live associations count
accepted 37
Invalid type: type must be "custody" 89
invalid signature length 41

What changed

1. Accept header.type: "auth". The specification says the type must be "custody" or
"auth", and across live manifests auth is now the majority — 89 to 78. Those 89 were
being rejected before their signature was even read.

Relaxing the type check alone would be a regression, though: an auth key is not the
custody address, so IdRegistry.custodyOf(fid) is the wrong binding check for it, and
accepting auth without branching would trade a false rejection for a false acceptance.
So the binding step now branches — custody keys resolve through custodyOf, auth keys
through the FID's hub verifications (/v1/verificationsByFid). A key that is the custody
address is accepted either way, since it binds the domain regardless of how the header
labels it.

This is the one design decision worth a second opinion: it introduces a hub fetch, with the
public hub as a new FARCASTER_HUB_URL constant. If you would rather source that from
Neynar, your own API, or make it configurable, say so and I will rework it.

2. Decode both signature encodings. fromBase64Url is atob, so the previous
fromBase64Url(sig) as Hex only worked when the field was base64url of the ASCII text
"0x…" — which is what this package's own useSignManifest emits (btoa of wagmi's hex
string). The canonical form is base64url of the raw signature bytes: it is what
@farcaster/miniapp-node's JFS codec both emits and parses, and it is 130 of the 167 live
manifests. For those, atob produced binary garbage and viem threw invalid signature length.

The net effect today is that the validator accepts precisely the manifests its own
generator produced. The spec page's two examples happen to use one encoding each, which is
probably how the split arose, so the new decodeSignature helper accepts both.

3. Verify against the public client. The hook called viem's top-level verifyMessage
the offline ecrecover utility — rather than the client action, despite already
constructing a public client ten lines later for custodyOf. Offline verification cannot
resolve ERC-1271, nor ERC-6492 for an account still counterfactual on the verifying chain,
so any smart-account signer was reported as a forgery.

No custody-type smart account happens to be in the current sample, so this one is latent
rather than firing today. turbo-gum.xyz (fid 452215) shows the shape of it — a 1440-byte
association ending in the ERC-6492 magic that fails offline and verifies true on OP
mainnet. Smart wallets are only getting more common.

Behaviour changes

  • type: "auth" is now accepted when the key is bound; previously always rejected.
  • The type error message is now Invalid type: type must be "custody" or "auth".
  • New error Invalid auth address: not verified for this FID when an auth key is not bound.
  • New error Could not reach a Farcaster hub to check the auth address — a hub outage
    fails closed rather than silently passing.
  • Invalid signature and Invalid custody address are unchanged.

Tests

useValidateManifest.test.tsx keeps all five existing cases (with the type-error message
and the verifyMessage mock updated to the client action) and adds four: an auth address
the FID has verified, one it has not, an auth-typed association signed by the custody
address, and a hub failure. base64.test.ts keeps its eight and adds five for
decodeSignature, covering both encodings, their equivalence, a long ERC-6492 signature,
and a raw payload that merely starts with the bytes 0x.

22 tests pass. Disclosure on how I ran them: I could not install the full monorepo on
the machine I work from, so I ran these two suites in an isolated vitest project against
the package's real sources (useValidateManifest.ts, base64.ts, constants.ts,
useSignManifest.ts unmodified from this branch). Please let CI be the arbiter for the
workspace run — if lint or type-check has opinions I have not anticipated, point me at them
and I will fix them.

I also ran the patched logic over all 167 live associations: 166 accepted, 1 rejected
sprinkles.wtf, whose declared custody key is neither fid 209951's custody address nor
among that FID's hub verifications. (faster-tasks.com, whose association is signed for
fastertasks.xyz, is accepted here because the hook does not check the serving domain —
see Not included below.)

Not included

No changeset — miniapp-manifest-generator is in the changesets ignore list.

The independent check that payload.domain matches the domain actually serving the
manifest is still absent from the hook. It is a different concern from this PR and the
generator may not know the serving domain at validation time, so I left it alone; happy to
add it separately if you want it.

…w-byte signatures

The validator rejects 130 of the 167 mini app manifests currently live in Farcaster's
directory. None of the 130 are invalid.

- Accept header.type "auth". The spec says the type must be "custody" or "auth", and
  across live manifests auth is now the majority (89 to 78). Because an auth key is not
  the custody address, the binding check branches on it too: custody keys resolve through
  IdRegistry.custodyOf, auth keys through the FID's hub verifications. Relaxing the type
  check without that would trade a false rejection for a false acceptance.
- Decode both signature encodings. The canonical form is base64url of the raw signature
  bytes -- what @farcaster/miniapp-node's JFS codec emits and parses, and 130 of 167 live
  manifests -- while the previous code only read base64url of the ASCII "0x..." string,
  which is what this package's own signer emits. The spec's two examples use one of each.
- Verify against the public client instead of viem's offline verifyMessage, so ERC-1271
  and ERC-6492 smart-account signatures resolve rather than being reported as forgeries.
  A public client was already being constructed for the custodyOf call.

Refs coinbase#2672
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

@agentatwork is attempting to deploy a commit to the Coinbase Team on Vercel.

A member of the Team first needs to authorize it.

@cb-heimdall

Copy link
Copy Markdown

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/2
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 1
Sum 2

@agentatwork

Copy link
Copy Markdown
Author

Correction to one number in the description, now fixed above: running the patched hook over the 167 live associations accepts 166, not 165. I had counted faster-tasks.com as a rejection, but its association is a valid signature for fastertasks.xyz — the hook does not check the serving domain, so it passes. The single rejection is sprinkles.wtf. Nothing else in the PR changes; flagging it rather than quietly editing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants