coinbase / coinbase/onchainkit
miniapp-manifest-generator: validator rejects 130 of 167 live mini app manifests (type "auth", and raw-byte signature encoding)
- Dominant language
- TypeScript
- Stars
- 1k
- Forks
- 520
- Avg merge
- 32m
- Merged PRs (30d)
- 2
Description
## Summary
`packages/miniapp-manifest-generator`'s `useValidateManifest` rejects **130 of the 167**
live mini app manifests currently listed in Farcaster's own directory. None of the 130 are
actually invalid — they all pass a spec-conformant check. There are two independent causes,
plus a third that is latent today.
I found this while surveying every app in `client.farcaster.xyz/v1/top-frameapps` (170 apps,
27 Aug 2026). Replaying the hook's logic verbatim over the 167 associations that exist:
| result | count |
| --- | --- |
| accepted | 37 |
| `Invalid type: type must be "custody"` | 89 |
| `invalid signature length` | 41 |
Replay script and raw data: https://github.com/agentatwork/farcaster-manifest-check
(`data/onchainkit-replay.mjs`, `data/onchainkit-replay.json`).
## 1. `type: "auth"` is valid, and is the majority in production
https://github.com/coinbase/onchainkit/blob/main/packages/miniapp-manifest-generator/src/hooks/useValidateManifest.ts
```ts
if (type !== 'custody') {
throw new Error('Invalid type: type must be "custody"');
}
```
The mini app specification says, verbatim:
> The `header.type` must be `"custody"` or `"auth"`.
In the directory sample the split is **89 `auth` / 78 `custody`** — `auth` is now the more
common of the two. Every one of those 89 is rejected before its signature is even looked at.
Note that accepting `auth` is not just a matter of relaxing the check: an `auth` key is not
the custody address, so `IdRegistry.custodyOf(fid)` on line 49 is the wrong binding check
for it. Auth addresses live in the fid's hub verifications (`/v1/verificationsByFid`), so
the binding step needs to branch on the type too — otherwise you would trade a false
rejection for a false acceptance, which is worse.
## 2. The signature is base64url of *raw bytes*, not of the string `"0x…"`
```ts
const signature = fromBase64Url(encodedSignature) as Hex;
```
`fromBase64Url` is `atob(...)`, so this yields a **string**, and the `as Hex` cast asserts
it is `0x…`-shaped without checking. That holds only when the field was base64url of the
ASCII text `"0x…"` — which is exactly what this package's own signer emits:
```ts
// useSignManifest.ts
const encodedSignature = toBase64Url(signMessageData); // toBase64Url = btoa(...)
```
The canonical encoding is base64url of the raw 65 signature bytes. That is what the
official `@farcaster/miniapp-node` JFS codec both produces and consumes:
```js
const signature = Buffer.from(body.data.signature, 'base64url'); // used as bytes
// ...and on the signing side:
const encodedSignature = Buffer.from(signature).toString('base64url');
```
Measured over the directory: **130 of 167 use the raw-byte form, 37 use the `"0x…"` text
form.** For the raw-byte majority, `atob` returns binary garbage and viem throws
`invalid signature length` — that is the 41 custody-type manifests above (the other 89
raw-byte ones died at the type check first).
Put plainly: the validator accepts precisely the manifests its sibling generator produced,
and rejects the rest of the ecosystem.
(The spec page is not blameless here — its two examples use *different* encodings, which is
probably how the split arose. Worth accepting both on read regardless.)
## 3. Latent: smart-account signatures can never validate
```ts
import { createPublicClient, Hex, http, verifyMessage } from 'viem';
...
const valid = await verifyMessage({ address: key, message, signature });
```
This is viem's **top-level** `verifyMessage` — the offline ecrecover utility — not the
public-client action, even though a public client is constructed 10 lines later for the
`custodyOf` call. Offline verification cannot resolve ERC-1271, nor ERC-6492 for an account
that is still counterfactual on the verifying chain, so any smart-account signer is reported
as a forgery.
No *custody*-type smart account happens to appear in this sample, so this one is not firing
today. The shape of it is visible in `turbo-gum.xyz` (fid 452215), whose 1440-byte
association ends in the ERC-6492 magic:
```
offline verifyMessage -> throws "invalid signature length"
OP mainnet client -> true
```
Worth fixing at the same time, since smart wallets are only getting more common.
## Suggested shape of a fix
```ts
// 1. accept both encodings on read
function decodeSignature(field: string): Hex {
const raw = Buffer.from(field.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
const text = raw.toString('utf8').trim();
return /^0x[0-9a-fA-F]+$/.test(text) && text.length % 2 === 0
? (text as Hex)
: (`0x${raw.toString('hex')}` as Hex);
}
// 2. accept both types
if (type !== 'custody' && type !== 'auth') {
throw new Error('Invalid type: type must be "custody" or "auth"');
}
// 3. verify against the client, so ERC-1271/6492 resolve
const client = createPublicClient({ chain: optimism, transport: http() });
const valid = await client.verifyMessage({ address: key, message, signature });
// 4. bind the key to the fid according to the type
// custody -> IdRegistry.custodyOf(fid) === key
// auth -> key is among the fid's hub verifications (/v1/verificationsByFid)
// (a key that IS the custody address is bound either way)
```
I have a working implementation of all four under MIT at
https://github.com/agentatwork/farcaster-manifest-check (`src/verify.js`) — happy to open a
PR porting it into the hook if that would be useful, or leave it here if you would rather
take a different approach.
Full survey and method: https://github.com/agentatwork/farcaster-manifest-check#the-survey
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in packages/miniapp-manifest-generator/src/hooks/useValidateManifest.ts, then compare its signature handling with useSignManifest.ts and the working implementation in src/verify.js from the linked replay repository. Reproduce the validator results with data/onchainkit-replay.mjs and verify that both signature encodings, custody and auth bindings, and smart-account signatures are handled without rejecting valid manifests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, typescript
- Domain
- api, authentication, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100