Skip to content

Commit 5a602e9

Browse files
committed
fix jfs and request formats
1 parent c6ecd14 commit 5a602e9

32 files changed

Lines changed: 619 additions & 596 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- The `pkgs/ui-elements` workspace package (`@farcaster/snap-ui-elements`) defines the json-render catalog; the emulator depends on it to render snaps.
1919
- Local dev ports: emulator on 3000, `snap-template` on 3003; example apps under `examples/` use ports 3010 and higher with a distinct port per app.
2020
- For snap HTTP GET, send `Accept: application/json+farcaster-snap`; example servers typically expose JSON on `/snap`, not on bare `/`.
21-
- For local POST in `snap-template`, set `BYPASS_SIGNATURE_VERIFICATION=1` to skip hub signature verification (also accepts `true`/`yes`).
21+
- Local `registerSnapHandler` skips JFS when `NODE_ENV` is not `production` (unsigned JSON POSTs, including from `apps/emulator`). Set `SKIP_JFS_VERIFICATION=no` to require JFS anyway, or `=yes` / `=1` to force bypass in production (dev-only).
2222
- When using `FARCASTER_HUB_URL`, include the port (e.g. `https://rho.farcaster.xyz:3381`).
2323
- Set `SNAP_PUBLIC_BASE_URL` to the canonical HTTPS origin (no trailing slash) so `page.buttons[].target` URLs resolve correctly.
2424
- Snap hub verification uses the Hubble **HTTP** API only (no gRPC client). Hub URL helpers accept `http`/`https` with an explicit port or bare `host:port`; `grpc:`/`grpcs:` are invalid.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ A local snap emulator where you paste a snap URL and interact with it.
2424

2525
This emulator does **not** sign its payload with real private keys, so emulated snaps must bypass signature verification in order to work.
2626

27-
An emulator with full signing is available inside the Farcaster web app.
27+
An emulator with JFS signing is available inside the Farcaster web app.
2828

2929
```bash
3030
pnpm --filter @farcaster/snap-emulator dev

agent-skills/create-farcaster-snap/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ When the user wants a **live** snap (not just JSON), use the workspace package *
8989
- Local dev: **`pnpm --filter snap-template dev`** runs **`src/server.ts`** (default port **3003**).
9090
- **Vercel / edge:** **`template/src/index.ts`** wires the same Hono app via `hono/vercel` (`handle`) for `GET`/`POST` exports — match whatever your host expects (see **`template/README.md`**).
9191
- Set **`SNAP_PUBLIC_BASE_URL`** to your deployment origin (no trailing slash) so `page.buttons[].target` URLs resolve correctly.
92-
- For local POST testing, set **`BYPASS_SIGNATURE_VERIFICATION=1`**.
92+
- For local POST testing, set **`SKIP_JFS_VERIFICATION=1`**.
9393

9494
Deploy (e.g. host.neynar.app): follow **`template/README.md`** — use the **`hono`** framework for this template, not Vite.
9595

apps/emulator/next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./.next/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

apps/emulator/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,25 @@
44
"private": true,
55
"scripts": {
66
"build": "next build",
7+
"build:workspace-deps": "pnpm --filter @farcaster/snap --filter @farcaster/snap-ui-elements run build",
78
"clean": "rm -rf .next",
9+
"prebuild": "pnpm run build:workspace-deps",
10+
"predev": "pnpm run build:workspace-deps",
811
"dev": "next dev -p 3000 --webpack",
912
"start": "next start -p 3000",
1013
"typecheck": "tsc --noEmit"
1114
},
1215
"dependencies": {
16+
"@farcaster/jfs": "0.2.2",
1317
"@farcaster/snap": "workspace:*",
1418
"@farcaster/snap-ui-elements": "workspace:*",
1519
"@json-render/core": "^0.15.0",
1620
"@json-render/react": "^0.15.0",
21+
"@noble/curves": "2.0.1",
1722
"next": "^16.2.0",
1823
"react": "^19.2.4",
1924
"react-dom": "^19.2.4",
25+
"viem": "^2.21.0",
2026
"zod": "^4.3.6"
2127
},
2228
"devDependencies": {

apps/emulator/src/app/api/snap/route.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { encodePayload } from "@farcaster/jfs";
12
import { NextRequest, NextResponse } from "next/server";
2-
import type { SnapPostRequestBody } from "@farcaster/snap";
3+
import type { SnapPayload } from "@farcaster/snap";
34
import {
45
coerceUpstreamUrlToMatchCurrentSnap,
56
parseSnapPayload,
@@ -18,7 +19,9 @@ async function readUpstreamJson(upstream: Response): Promise<unknown> {
1819
} catch {
1920
const sample = trimmed.length > 200 ? `${trimmed.slice(0, 200)}…` : trimmed;
2021
throw new Error(
21-
`Snap response is not JSON (${upstream.headers.get("content-type") ?? "unknown type"}). Body: ${sample}`,
22+
`Snap response is not JSON (${
23+
upstream.headers.get("content-type") ?? "unknown type"
24+
}). Body: ${sample}`,
2225
);
2326
}
2427
}
@@ -153,14 +156,19 @@ export async function POST(request: NextRequest) {
153156

154157
const userFid = normalizeUserFid(body.fid);
155158
const timestamp = Math.floor(Date.now() / 1000);
156-
const wireBody: SnapPostRequestBody = {
159+
const payload: SnapPayload = {
157160
fid: userFid,
158-
inputs: (body.inputs ?? {}) as SnapPostRequestBody["inputs"],
161+
inputs: (body.inputs ?? {}) as SnapPayload["inputs"],
159162
button_index: body.buttonIndex ?? 0,
160163
timestamp,
161164
};
162165

163-
const postBody = JSON.stringify(wireBody);
166+
const jfsEnvelope = {
167+
header: "dev",
168+
payload: encodePayload(payload),
169+
signature: "dev",
170+
};
171+
const postBody = JSON.stringify(jfsEnvelope);
164172
const contentType = "application/json; charset=utf-8";
165173

166174
try {

apps/emulator/src/app/page.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type LogPairResponse = {
3838
networkError?: boolean;
3939
/** From `Content-Type` when a `Response` was received; omitted for network failures. */
4040
contentType?: string | null;
41-
/** Authoritative proxy → snap request from `/api/snap` (real signature, POST body, final URL). */
41+
/** Authoritative proxy → snap request from `/api/snap` (JFS-shaped JSON POST: header/payload/signature, final URL). */
4242
resolvedOutboundToSnap?: OutboundSnapRequestLog;
4343
};
4444

@@ -257,8 +257,8 @@ function splitEmulatorDebugForLog(parsed: unknown): {
257257
d.upstreamSnapMethod === "POST"
258258
? "POST"
259259
: d.upstreamSnapMethod === "GET"
260-
? "GET"
261-
: null;
260+
? "GET"
261+
: null;
262262
const { _emulatorDebug: _, ...rest } = o;
263263
if (!method || !url || !headers) {
264264
return { bodyForLog: rest };
@@ -1016,8 +1016,9 @@ export default function EmulatorPage() {
10161016
type="button"
10171017
onClick={() => {
10181018
void (async () => {
1019-
const ok =
1020-
await copyToClipboard(outboundToSnapText);
1019+
const ok = await copyToClipboard(
1020+
outboundToSnapText,
1021+
);
10211022
if (!ok) return;
10221023
flashCopied(outboundPartKey);
10231024
})();
@@ -1116,8 +1117,9 @@ export default function EmulatorPage() {
11161117
type="button"
11171118
onClick={() => {
11181119
void (async () => {
1119-
const ok =
1120-
await copyToClipboard(emulatorHeadersText);
1120+
const ok = await copyToClipboard(
1121+
emulatorHeadersText,
1122+
);
11211123
if (!ok) return;
11221124
flashCopied(emulatorHeadersPartKey);
11231125
})();

examples/current-time/api/index.ts

Lines changed: 37 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,53 +8,47 @@ const OPT_LOCAL = "Local";
88

99
const app = new Hono();
1010

11-
registerSnapHandler(
12-
app,
13-
async ({ action }) => {
14-
const pref =
15-
action.type === "post" &&
16-
typeof action.inputs[BUTTON_GROUP_NAME] === "string"
17-
? (action.inputs[BUTTON_GROUP_NAME] as string)
18-
: undefined;
19-
const body = timeBody(pref);
20-
return {
21-
version: "1.0",
22-
page: {
23-
theme: { accent: "blue" },
24-
button_layout: "stack",
25-
elements: {
26-
type: "stack" as const,
27-
children: [
28-
{ type: "text", style: "title", content: "Server time" },
29-
{ type: "text", style: "body", content: body },
30-
{
31-
type: "button_group",
32-
name: BUTTON_GROUP_NAME,
33-
options: [OPT_ISO, OPT_LOCAL],
34-
style: "row",
35-
},
36-
{
37-
type: "text",
38-
style: "caption",
39-
content:
40-
"Choose format, then refresh. Time is from this server when it responds.",
41-
},
42-
],
43-
},
44-
buttons: [
11+
registerSnapHandler(app, async ({ action }) => {
12+
const pref =
13+
action.type === "post" &&
14+
typeof action.inputs[BUTTON_GROUP_NAME] === "string"
15+
? (action.inputs[BUTTON_GROUP_NAME] as string)
16+
: undefined;
17+
const body = timeBody(pref);
18+
return {
19+
version: "1.0",
20+
page: {
21+
theme: { accent: "blue" },
22+
button_layout: "stack",
23+
elements: {
24+
type: "stack" as const,
25+
children: [
26+
{ type: "text", style: "title", content: "Server time" },
27+
{ type: "text", style: "body", content: body },
28+
{
29+
type: "button_group",
30+
name: BUTTON_GROUP_NAME,
31+
options: [OPT_ISO, OPT_LOCAL],
32+
style: "row",
33+
},
4534
{
46-
label: "Refresh",
47-
action: "post",
48-
target: `${snapBaseUrl()}/`,
35+
type: "text",
36+
style: "caption",
37+
content:
38+
"Choose format, then refresh. Time is from this server when it responds.",
4939
},
5040
],
5141
},
52-
};
53-
},
54-
{
55-
bypassSignatureVerification: bypassSignatureVerification(),
56-
},
57-
);
42+
buttons: [
43+
{
44+
label: "Refresh",
45+
action: "post",
46+
target: `${snapBaseUrl()}/`,
47+
},
48+
],
49+
},
50+
};
51+
});
5852

5953
export default app;
6054

@@ -69,11 +63,6 @@ function snapBaseUrl(): string {
6963
return raw.replace(/\/$/, "");
7064
}
7165

72-
function bypassSignatureVerification(): boolean {
73-
const v = process.env.BYPASS_SIGNATURE_VERIFICATION?.trim().toLowerCase();
74-
return v === "1" || v === "true" || v === "yes";
75-
}
76-
7766
function timeBody(pref: string | undefined): string {
7867
const now = new Date();
7968
if (pref === OPT_LOCAL) {

examples/current-time/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"packageManager": "pnpm@9.15.4",
55
"scripts": {
66
"predev": "pnpm --filter @farcaster/snap run build",
7-
"dev": "tsx watch src/server.ts",
7+
"dev": "SKIP_JFS_VERIFICATION=true tsx watch src/server.ts",
88
"build": "tsc --noEmit",
99
"typecheck": "tsc --noEmit"
1010
},

0 commit comments

Comments
 (0)