From 8edbb2514a2196f9f1df46c6db3c280fac786554 Mon Sep 17 00:00:00 2001 From: Christian Mladenov Date: Thu, 12 Dec 2024 11:23:51 -0800 Subject: [PATCH] Switch to using @farcaster/frame-node for webhook parsing & validation --- package.json | 3 +- src/app/api/webhook/route.ts | 63 +++++++++++++++++++----------------- src/lib/jfs.ts | 62 ----------------------------------- src/lib/neynar.ts | 34 ------------------- yarn.lock | 26 +++++++++------ 5 files changed, 53 insertions(+), 135 deletions(-) delete mode 100644 src/lib/jfs.ts delete mode 100644 src/lib/neynar.ts diff --git a/package.json b/package.json index acb3419..e0cea1c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "lint": "next lint" }, "dependencies": { - "@farcaster/frame-sdk": "^0.0.14", + "@farcaster/frame-node": "^0.0.3", + "@farcaster/frame-sdk": "^0.0.16", "@tanstack/react-query": "^5.61.0", "@upstash/redis": "^1.34.3", "next": "15.0.3", diff --git a/src/app/api/webhook/route.ts b/src/app/api/webhook/route.ts index 91aef3e..2bdc294 100644 --- a/src/app/api/webhook/route.ts +++ b/src/app/api/webhook/route.ts @@ -1,6 +1,9 @@ -import { eventPayloadSchema } from "@farcaster/frame-sdk"; +import { + ParseWebhookEvent, + parseWebhookEvent, + verifyAppKeyWithNeynar, +} from "@farcaster/frame-node"; import { NextRequest } from "next/server"; -import { verifyJsonFarcasterSignature } from "~/lib/jfs"; import { deleteUserNotificationDetails, setUserNotificationDetails, @@ -12,38 +15,40 @@ export async function POST(request: NextRequest) { let data; try { - const verifySignatureResult = await verifyJsonFarcasterSignature( - requestJson - ); - if (verifySignatureResult.success === false) { - return Response.json( - { success: false, error: verifySignatureResult.error }, - { status: 401 } - ); - } + data = await parseWebhookEvent(requestJson, verifyAppKeyWithNeynar); + } catch (e: unknown) { + const error = e as ParseWebhookEvent.ErrorType; - data = verifySignatureResult; - } catch { - return Response.json({ success: false }, { status: 500 }); + switch (error.name) { + case "VerifyJsonFarcasterSignature.InvalidDataError": + case "VerifyJsonFarcasterSignature.InvalidEventDataError": + // The request data is invalid + return Response.json( + { success: false, error: error.message }, + { status: 400 } + ); + case "VerifyJsonFarcasterSignature.InvalidAppKeyError": + // The app key is invalid + return Response.json( + { success: false, error: error.message }, + { status: 401 } + ); + case "VerifyJsonFarcasterSignature.VerifyAppKeyError": + // Internal error verifying the app key (caller may want to try again) + return Response.json( + { success: false, error: error.message }, + { status: 500 } + ); + } } const fid = data.fid; - const payloadData = JSON.parse( - Buffer.from(data.payload, "base64url").toString("utf-8") - ); - const payload = eventPayloadSchema.safeParse(payloadData); + const event = data.event; - if (payload.success === false) { - return Response.json( - { success: false, errors: payload.error.errors }, - { status: 400 } - ); - } - - switch (payload.data.event) { + switch (event.event) { case "frame_added": - if (payload.data.notificationDetails) { - await setUserNotificationDetails(fid, payload.data.notificationDetails); + if (event.notificationDetails) { + await setUserNotificationDetails(fid, event.notificationDetails); await sendFrameNotification({ fid, title: "Welcome to Frames v2", @@ -59,7 +64,7 @@ export async function POST(request: NextRequest) { break; case "notifications_enabled": - await setUserNotificationDetails(fid, payload.data.notificationDetails); + await setUserNotificationDetails(fid, event.notificationDetails); await sendFrameNotification({ fid, title: "Ding ding ding", diff --git a/src/lib/jfs.ts b/src/lib/jfs.ts deleted file mode 100644 index eb199f8..0000000 --- a/src/lib/jfs.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ed25519 } from "@noble/curves/ed25519"; -import { - encodedJsonFarcasterSignatureSchema, - jsonFarcasterSignatureHeaderSchema, -} from "@farcaster/frame-sdk"; -import { isSignerValid } from "~/lib/neynar"; - -type VerifyJsonFarcasterSignatureResult = - | { success: false; error: unknown } - | { success: true; fid: number; payload: string }; - -export async function verifyJsonFarcasterSignature( - data: unknown -): Promise { - // Parse & decode - const body = encodedJsonFarcasterSignatureSchema.safeParse(data); - if (body.success === false) { - return { success: false, error: body.error.errors }; - } - - const headerData = JSON.parse( - Buffer.from(body.data.header, "base64url").toString("utf-8") - ); - const header = jsonFarcasterSignatureHeaderSchema.safeParse(headerData); - if (header.success === false) { - return { success: false, error: header.error.errors }; - } - - const signature = Buffer.from(body.data.signature, "base64url"); - if (signature.byteLength !== 64) { - return { success: false, error: "Invalid signature length" }; - } - - const fid = header.data.fid; - const key = header.data.key; - - // Verify that the signer belongs to the FID - try { - const validSigner = await isSignerValid({ - fid, - signerPublicKey: key, - }); - if (!validSigner) { - return { success: false, error: "Invalid signer" }; - } - } catch { - return { success: false, error: "Error verifying signer" }; - } - - const signedInput = new Uint8Array( - Buffer.from(body.data.header + "." + body.data.payload) - ); - - const keyBytes = Uint8Array.from(Buffer.from(key.slice(2), "hex")); - - const verifyResult = ed25519.verify(signature, signedInput, keyBytes); - if (!verifyResult) { - return { success: false, error: "Invalid signature" }; - } - - return { success: true, fid, payload: body.data.payload }; -} diff --git a/src/lib/neynar.ts b/src/lib/neynar.ts deleted file mode 100644 index 772d29b..0000000 --- a/src/lib/neynar.ts +++ /dev/null @@ -1,34 +0,0 @@ -const apiKey = process.env.NEYNAR_API_KEY || ""; - -export async function isSignerValid({ - fid, - signerPublicKey, -}: { - fid: number; - signerPublicKey: string; -}): Promise { - const url = new URL("https://hub-api.neynar.com/v1/onChainSignersByFid"); - url.searchParams.append("fid", fid.toString()); - - const response = await fetch(url, { - method: "GET", - headers: { - "x-api-key": apiKey, - }, - }); - - if (response.status !== 200) { - throw new Error(await response.text()); - } - - const responseJson = await response.json(); - const signerPublicKeyLC = signerPublicKey.toLowerCase(); - - const signerExists = responseJson.events.find( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (event: any) => - event.signerEventBody.key.toLowerCase() === signerPublicKeyLC - ); - - return signerExists; -} diff --git a/yarn.lock b/yarn.lock index 65c4107..9b84889 100644 --- a/yarn.lock +++ b/yarn.lock @@ -105,20 +105,28 @@ ethereum-cryptography "^2.0.0" micro-ftch "^0.3.1" -"@farcaster/frame-core@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.13.tgz#cd4739488a7492f9b4f7b302f0220e8955b3d4aa" - integrity sha512-PXs2SJnt8GAKw4JspfBSWLKfvx1mGiWGZ8aB46ehMQMUtsN7k5gzSIdgWLpN47OeVEsi1X86fW6i8YkoL6L/nQ== +"@farcaster/frame-core@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.15.tgz#05a4ed6f7c0d43d2f41d13714fb8b13419068a11" + integrity sha512-WQfAEqyQAz3EzEdfqAMV7s2VMIYBGWz0Qt5CUUkmSelvv0a+8A61YmBnpemCi3NEwWzEJBTc/IxzQ29w2axPBg== dependencies: ox "^0.4.0" zod "^3.23.8" -"@farcaster/frame-sdk@^0.0.14": - version "0.0.14" - resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.14.tgz#427a9156263e8bab5096af8751ed3f6bbcaef1fa" - integrity sha512-FPQn1LeEQbswoLpYnebrFv+UWFKV/thUX6UEn3LGWZTZyrYFPbtvJA6UHKJd8wgvU2o+/RJ5+P7tuxFcXLjHBg== +"@farcaster/frame-node@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@farcaster/frame-node/-/frame-node-0.0.3.tgz#4502694fef336cefd56b1aa5642128844ac62668" + integrity sha512-ReZvQozIXn1pwAn56/5ms54Gw+jwWP6TW+rmlvaX4hDi7pJATnHJvwZ/nZw8yj2GsWPNr2DxUJlC0Zt0UORN/Q== dependencies: - "@farcaster/frame-core" "^0.0.13" + "@farcaster/frame-core" "^0.0.15" + ox "^0.4.0" + +"@farcaster/frame-sdk@^0.0.16": + version "0.0.16" + resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.16.tgz#1e3dc191f950065d2f38daf61dc45775d7073421" + integrity sha512-043j2EiCOaHvS1ox0yVx7KZQU5LB1P+39waK3xtX0knNKQ54wZxCOTcca0kUcJfTBN5db7iVPOh/E+LJ5kjDew== + dependencies: + "@farcaster/frame-core" "^0.0.15" comlink "^4.4.2" eventemitter3 "^5.0.1" ox "^0.4.0"