Skip to content

Commit 6ec3ea1

Browse files
committed
fix publish bug
1 parent 520e4dc commit 6ec3ea1

8 files changed

Lines changed: 61 additions & 15 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-
- 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).
21+
- Local `registerSnapHandler` skips JFS **signature** verification when `NODE_ENV` is not `production`, but POST bodies must still be JFS-shaped JSON (`header` / `payload` / `signature`), including from `apps/emulator`. Set `SKIP_JFS_VERIFICATION=no` to require verification 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.

pkgs/hono/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"hono": ">=4.0.0"
4040
},
4141
"devDependencies": {
42+
"@farcaster/jfs": "^0.2.2",
4243
"@types/node": "^25.5.0",
4344
"hono": "^4.0.0",
4445
"typescript": "^5.4.0",

pkgs/hono/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type SnapHandlerOptions = {
1616
path?: string;
1717

1818
/**
19-
* When true, skip JFS verification of JSON POST bodies.
19+
* When true, skip JFS signature verification only. POST bodies must still be JFS-shaped JSON.
2020
* When omitted, default to {@link envSkipJFSVerification}.
2121
*/
2222
skipJFSVerification?: boolean;
@@ -32,8 +32,8 @@ export type SnapHandlerOptions = {
3232
* Register GET and POST snap handlers on `app` at `options.path` (default `/`).
3333
*
3434
* - GET → calls `snapFn({ action: { type: "get" }, request })` and returns the response.
35-
* - POST → parses the request, verifies the JFS body via {@link verifyJFSRequestBody} (unless `skipJFSVerification` is true),
36-
* calls `snapFn({ action, request })`, and returns the response.
35+
* - POST → parses the JFS-shaped JSON body; verifies it via {@link verifyJFSRequestBody} unless
36+
* `skipJFSVerification` is true, then calls `snapFn({ action, request })` and returns the response.
3737
*
3838
* All parsing, schema validation, signature verification, and error responses
3939
* are handled automatically. `SnapContext.request` is the raw `Request` so handlers

pkgs/hono/tests/registerSnapHandler.test.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { describe, expect, it } from "vitest";
22
import { Hono } from "hono";
33
import { registerSnapHandler } from "../src/index";
4+
import { encodePayload } from "@farcaster/jfs";
45
import {
56
MEDIA_TYPE,
67
DEFAULT_THEME_ACCENT,
78
type SnapFunction,
9+
type SnapPayload,
810
} from "@farcaster/snap";
911

1012
const SNAP_CONTENT_TYPE = `${MEDIA_TYPE}; charset=utf-8`;
@@ -21,12 +23,17 @@ const minimalSnapFn: SnapFunction = async () => ({
2123
},
2224
});
2325

24-
function postBody() {
25-
return JSON.stringify({
26+
function jfsPostBody() {
27+
const payload: SnapPayload = {
2628
fid: 1,
2729
inputs: {},
2830
button_index: 0,
2931
timestamp: Math.floor(Date.now() / 1000),
32+
};
33+
return JSON.stringify({
34+
header: "dev",
35+
payload: encodePayload(payload),
36+
signature: "dev",
3037
});
3138
}
3239

@@ -67,13 +74,34 @@ describe("registerSnapHandler content type", () => {
6774
const res = await app.request("/", {
6875
method: "POST",
6976
headers: { "Content-Type": "application/json" },
70-
body: postBody(),
77+
body: jfsPostBody(),
7178
});
7279

7380
expect(res.status).toBe(200);
7481
expect(res.headers.get("Content-Type")).toBe(SNAP_CONTENT_TYPE);
7582
});
7683

84+
it("POST with bare JSON body returns 400 when skipping JFS verification", async () => {
85+
const app = new Hono();
86+
registerSnapHandler(app, minimalSnapFn, {
87+
skipJFSVerification: true,
88+
});
89+
90+
const res = await app.request("/", {
91+
method: "POST",
92+
headers: { "Content-Type": "application/json" },
93+
body: JSON.stringify({
94+
fid: 1,
95+
inputs: {},
96+
button_index: 0,
97+
timestamp: Math.floor(Date.now() / 1000),
98+
}),
99+
});
100+
101+
expect(res.status).toBe(400);
102+
expect(res.headers.get("Content-Type")).toMatch(/^application\/json/);
103+
});
104+
77105
it("POST with invalid body returns application/json (not snap type)", async () => {
78106
const app = new Hono();
79107
registerSnapHandler(app, minimalSnapFn, {

pkgs/snap/src/http.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ export type ParseRequestError =
3737

3838
export type ParseRequestOptions = {
3939
/**
40-
* When true, skip JFS verification of JSON POST bodies.
41-
* When false (default), the body must be JSON `{ header, payload, signature }` verified via
42-
* {@link verifyJFSRequestBody}.
40+
* When true, skip {@link verifyJFSRequestBody} (signature checks).
4341
*/
4442
skipJFSVerification?: boolean;
4543
};
@@ -68,8 +66,7 @@ const requestBodySchema = z.object({
6866
/**
6967
* Parse and validate Farcaster snap requests:
7068
* - `GET` is allowed for first-page loads and returns `{ type: "get" }`.
71-
* - `POST`: by default the body is JSON JFS (`header` / `payload` / `signature`) verified with
72-
* {@link verifyJFSRequestBody}; with `skipJFSVerification`, accepts plain action JSON (dev only).
69+
* - `POST`: the body must be JSON in JFS form (`header` / `payload` / `signature`) even if JFS verification is skipped.
7370
*/
7471
export async function parseRequest(
7572
request: Request,

pkgs/snap/tests/http.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe("parseRequest", () => {
3131
expect(res).toEqual({ success: true, action: { type: "get" } });
3232
});
3333

34-
it("accepts plain JSON POST when skipJFSVerification is true", async () => {
34+
it("accepts JFS-shaped POST even when skipJFSVerification is true", async () => {
3535
const body = postBody();
3636
const payload = decodePayload<SnapPayload>(body.payload);
3737
const res = await parseRequest(
@@ -54,6 +54,25 @@ describe("parseRequest", () => {
5454
});
5555
});
5656

57+
it("does not accept bare JSON POST payload even when skipJFSVerification is true", async () => {
58+
const payload: SnapPayload = {
59+
fid: 42,
60+
inputs: { guess: "HELLO" },
61+
button_index: 0,
62+
timestamp: Math.floor(Date.now() / 1000),
63+
};
64+
const res = await parseRequest(
65+
new Request("https://example.com/snap", {
66+
method: "POST",
67+
headers: { "Content-Type": "application/json" },
68+
body: JSON.stringify(payload),
69+
}),
70+
{ skipJFSVerification: true },
71+
);
72+
expect(res.success).toBe(false);
73+
if (!res.success) expect(res.error.type).toBe("invalid_json");
74+
});
75+
5776
it("fails when bypass JSON is invalid", async () => {
5877
const res = await parseRequest(
5978
new Request("https://example.com/snap", {

template/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ Then set dependencies in `package.json` to published versions of `@farcaster/sna
1717

1818
- Hono app with `@farcaster/snap-hono` (`registerSnapHandler`) for GET/POST handling and validation
1919
- Vercel entrypoint at `api/index.ts`
20-
- `registerSnapHandler` uses JFS verification in production (`NODE_ENV=production`); locally it accepts the same unsigned JSON body the emulator sends (override with `SKIP_JFS_VERIFICATION=no` to force JFS, or `=yes` to force bypass)
20+
- `registerSnapHandler` verifies JFS signatures in production (`NODE_ENV=production`); locally it skips verification for the same JFS-shaped dev envelope the emulator sends (override with `SKIP_JFS_VERIFICATION=no` to require verification, or `=yes` to force bypass)
2121
- Vercel deployment via `vercel.json`
2222

2323
## Endpoints
2424

2525
- `GET /` without `Accept: application/json+farcaster-snap` returns a short plain-text hint for browsers
2626
- `GET /` with the snap Accept header returns the first page (counter demo starting at 0)
27-
- `POST /` accepts a snap interaction payload (unsigned JSON in non-production; JFS in production) and returns the next page
27+
- `POST /` accepts a JFS-shaped snap interaction payload (signature verified in production only by default) and returns the next page
2828
- Response pages are kept within current spec limits (max 5 elements, text length constraints)
2929

3030
## Local development

turbo.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"outputs": ["dist/**", ".next/**"]
88
},
99
"test": {
10+
"dependsOn": ["^build"],
1011
"outputs": []
1112
},
1213
"typecheck": {

0 commit comments

Comments
 (0)