coinbase / coinbase/onchainkit

miniapp-manifest-generator: validator rejects 130 of 167 live mini app manifests (type "auth", and raw-byte signature encoding)

Open
#2,672 1 comment 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.