Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions apps/emulator/mdx-components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { MDXComponents } from "mdx/types";
import CodeBlock from "@/components/CodeBlock";
import InteractiveGrid from "@/components/docs/InteractiveGrid";
import InteractiveButtonGroup from "@/components/docs/InteractiveButtonGroup";
import SnapCard from "@/components/docs/SnapCard";
import ConfettiDemo from "@/components/docs/ConfettiDemo";

function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
}

function extractText(children: React.ReactNode): string {
if (typeof children === "string") return children;
if (Array.isArray(children)) return children.map(extractText).join("");
if (
children &&
typeof children === "object" &&
"props" in children
) {
const props = (children as { props?: Record<string, unknown> }).props;
if (props?.children) {
return extractText(props.children as React.ReactNode);
}
}
return "";
}

function Heading({
level,
children,
...props
}: {
level: 1 | 2 | 3 | 4;
children?: React.ReactNode;
} & React.HTMLAttributes<HTMLHeadingElement>) {
const Tag = `h${level}` as "h1" | "h2" | "h3" | "h4";
const text = extractText(children);
const id = slugify(text);

return (
<Tag id={id} {...props}>
<a href={`#${id}`} className="heading-anchor">
{children}
</a>
</Tag>
);
}

export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
h1: (props) => <Heading level={1} {...props} />,
h2: (props) => <Heading level={2} {...props} />,
h3: (props) => <Heading level={3} {...props} />,
h4: (props) => <Heading level={4} {...props} />,
pre: (props) => <CodeBlock {...props} />,
InteractiveGrid,
InteractiveButtonGroup,
SnapCard,
ConfettiDemo,
};
}
14 changes: 12 additions & 2 deletions apps/emulator/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import type { NextConfig } from "next";
import createMDX from "@next/mdx";
import remarkGfm from "remark-gfm";

const nextConfig: NextConfig = {};
const nextConfig: NextConfig = {
pageExtensions: ["ts", "tsx", "md", "mdx"],
};

export default nextConfig;
const withMDX = createMDX({
options: {
remarkPlugins: [remarkGfm],
},
});

export default withMDX(nextConfig);
8 changes: 7 additions & 1 deletion apps/emulator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@farcaster/snap-emulator",
"private": true,
"scripts": {
"build": "next build",
"build": "next build --webpack",
"build:workspace-deps": "pnpm --filter @farcaster/snap --filter @farcaster/snap-ui-elements run build",
"clean": "rm -rf .next",
"prebuild": "pnpm run build:workspace-deps",
Expand All @@ -17,10 +17,16 @@
"@farcaster/snap-ui-elements": "workspace:*",
"@json-render/core": "^0.15.0",
"@json-render/react": "^0.15.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.2.1",
"@noble/curves": "2.0.1",
"@types/mdx": "^2.0.13",
"lucide-react": "^1.7.0",
"next": "^16.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"remark-gfm": "^4.0.1",
"viem": "^2.21.0",
"zod": "^4.3.6"
},
Expand Down
113 changes: 113 additions & 0 deletions apps/emulator/src/app/docs/auth/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Authentication

Every POST request from the client to a snap server MUST be authenticated with **JSON Farcaster Signatures (JFS)**.

## How It Works

When a user taps a `post` button, the Farcaster client:

1. Collects all input values from the current page
2. Builds a payload with the user's FID, inputs, button index, and timestamp
3. Signs the payload using the user's Farcaster signer key
4. Sends the signed JFS compact string as the POST body

The snap server then:

1. Verifies the JFS signature cryptographically
2. Checks the signing key against hub state for the claimed FID
3. Validates the timestamp for replay protection
4. Processes the request and returns a new page

## JFS Payload Shape

The decoded JFS payload (signed inside JFS, not sent as bare JSON):

```json
{
"fid": 12345,
"inputs": {
"guess": "CLASS",
"vote": "Tabs"
},
"button_index": 0,
"timestamp": 1710864000
}
```

## JSON Farcaster Signatures (JFS) Format

JFS is a standardized way for Farcaster identities to sign arbitrary payloads. It consists of three components:

1. **Header** -- metadata (FID, key type, key)
2. **Payload** -- the content being signed
3. **Signature** -- the cryptographic signature

### Compact Serialization

JFS uses a dot-separated format similar to JWT:

```
BASE64URL(header) . BASE64URL(payload) . BASE64URL(signature)
```

The signing input is constructed as:

```
ASCII(BASE64URL(UTF8(Header)) || '.' || BASE64URL(Payload))
```

### Key Types

JFS supports three key types:

| Type | Signature Method | Description |
| --- | --- | --- |
| `custody` | ERC-191 | Signature from the FID's custody address |
| `auth` | ERC-191 | Signature from a registered auth address |
| `app_key` | EdDSA | Signature from a registered App Key |

For snaps, the client typically uses `app_key` (EdDSA signature from the user's registered signer key).

### Verification

To verify a JFS:

1. Decode the header and extract the `fid`, `type`, and `key`
2. Verify the FID is registered and the key is active for that FID
3. Verify the signature matches the signing input using the declared key
4. Query a Farcaster Hub to confirm the key is currently associated with the FID

### Reference Implementation

The official Node.js package is [`@farcaster/jfs`](https://github.com/farcasterxyz/auth-monorepo).

## Replay Protection

The request payload MUST contain a `timestamp` field (Unix seconds).

Servers SHOULD reject requests with timestamps outside an allowed skew (for example, 5 minutes) to limit replay attacks.

## Requirements

- The client MUST send a valid JFS for every authenticated POST
- The server MUST verify the JFS cryptographically and MUST verify the signing key against hub (or equivalent) state for the FID
- The server SHOULD enforce replay protection using `timestamp` (and any other policy you require)

## Server-Side Verification with @farcaster/snap-hono

The `@farcaster/snap-hono` package handles JFS verification automatically:

```typescript
import { registerSnapHandler } from "@farcaster/snap-hono";

registerSnapHandler(app, async ({ action, request }) => {
// action.fid is verified — the JFS signature was checked
// action.inputs contains the user's input values
// action.buttonIndex is which button was tapped
}, {
skipJfsVerification: false, // true for local dev
maxSkewSeconds: 300, // 5 minute replay window
});
```

Set `SKIP_JFS_VERIFICATION=1` in your environment for local development to skip JFS verification.
103 changes: 103 additions & 0 deletions apps/emulator/src/app/docs/building/page.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Building a Snap

There are several ways to create a Farcaster Snap, from AI-assisted generation to manual implementation.

## Claude Code Skill

If you use [Claude Code](https://claude.ai/code), you can install a custom slash command that generates snaps from natural language:

```bash
# Install the skill into your project
mkdir -p .claude/commands
curl -sS -o .claude/commands/create-farcaster-snap.md \
https://raw.githubusercontent.com/farcasterxyz/snap/main/agent-skills/create-farcaster-snap/SKILL.md
```

You can also [view the skill source on GitHub](https://github.com/farcasterxyz/snap/blob/main/agent-skills/create-farcaster-snap/SKILL.md).

Then run it:

```
/create-farcaster-snap a poll asking users to pick their favorite L2
```

The skill will:
1. Read the full snap spec
2. Generate valid snap JSON and server logic
3. Validate the output against the schema
4. Optionally deploy to a live URL

This is the fastest way to go from idea to working snap.

> **Note:** The skill works best when run from within the [snap](https://github.com/farcasterxyz/snap) repo, since it references the spec docs and template locally.

## Template (Hono)

The `snap-template/` directory is a starter project using [Hono](https://hono.dev) with the `@farcaster/snap-hono` package:

```bash
# From the repo root
cp -r snap-template my-snap
cd my-snap
pnpm install
```

Edit `src/app.ts` to implement your snap logic:

```typescript
import { Hono } from "hono";
import { registerSnapHandler } from "@farcaster/snap-hono";

const app = new Hono();

registerSnapHandler(app, async ({ action, request }) => {
if (action.type === "get") {
return {
version: "1.0",
page: {
theme: { accent: "purple" },
elements: {
type: "stack",
children: [
{ type: "text", style: "title", content: "My Snap" },
{ type: "text", style: "body", content: "Hello world" },
],
},
},
};
}

// Handle POST interactions
const { fid, inputs, buttonIndex } = action;
// ... your logic here
}, {
skipJfsVerification: process.env.SKIP_JFS_VERIFICATION === "1",
});
```

Run locally:

```bash
SKIP_JFS_VERIFICATION=1 pnpm dev # http://localhost:3003
```

## Testing

Use the [Emulator](https://farcaster.xyz/~/developers/snaps) to test your snap. Enter your snap's URL and interact with it -- the emulator signs messages automatically, so no signature bypass is needed.

## Deploying

Snaps can be deployed anywhere that serves HTTP. Common options:

- **Vercel** -- works with the Hono template out of the box
- **Any Node.js host** -- the Hono template includes a standalone server

Set `SNAP_PUBLIC_BASE_URL` to your deployment origin (no trailing slash) so button target URLs resolve correctly.

After deploying, verify your snap works:

```bash
curl -sS -H 'Accept: application/vnd.farcaster.snap+json' https://your-snap-url.com/
```

You should get valid JSON with content type `application/vnd.farcaster.snap+json`.
Loading
Loading