mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-11-16 08:08:56 -05:00
Switch to using @farcaster/frame-node for webhook parsing & validation
This commit is contained in:
parent
8acf07b03e
commit
8edbb2514a
@ -9,7 +9,8 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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",
|
"@tanstack/react-query": "^5.61.0",
|
||||||
"@upstash/redis": "^1.34.3",
|
"@upstash/redis": "^1.34.3",
|
||||||
"next": "15.0.3",
|
"next": "15.0.3",
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import { eventPayloadSchema } from "@farcaster/frame-sdk";
|
import {
|
||||||
|
ParseWebhookEvent,
|
||||||
|
parseWebhookEvent,
|
||||||
|
verifyAppKeyWithNeynar,
|
||||||
|
} from "@farcaster/frame-node";
|
||||||
import { NextRequest } from "next/server";
|
import { NextRequest } from "next/server";
|
||||||
import { verifyJsonFarcasterSignature } from "~/lib/jfs";
|
|
||||||
import {
|
import {
|
||||||
deleteUserNotificationDetails,
|
deleteUserNotificationDetails,
|
||||||
setUserNotificationDetails,
|
setUserNotificationDetails,
|
||||||
@ -12,38 +15,40 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
const verifySignatureResult = await verifyJsonFarcasterSignature(
|
data = await parseWebhookEvent(requestJson, verifyAppKeyWithNeynar);
|
||||||
requestJson
|
} catch (e: unknown) {
|
||||||
);
|
const error = e as ParseWebhookEvent.ErrorType;
|
||||||
if (verifySignatureResult.success === false) {
|
|
||||||
return Response.json(
|
|
||||||
{ success: false, error: verifySignatureResult.error },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
data = verifySignatureResult;
|
switch (error.name) {
|
||||||
} catch {
|
case "VerifyJsonFarcasterSignature.InvalidDataError":
|
||||||
return Response.json({ success: false }, { status: 500 });
|
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 fid = data.fid;
|
||||||
const payloadData = JSON.parse(
|
const event = data.event;
|
||||||
Buffer.from(data.payload, "base64url").toString("utf-8")
|
|
||||||
);
|
|
||||||
const payload = eventPayloadSchema.safeParse(payloadData);
|
|
||||||
|
|
||||||
if (payload.success === false) {
|
switch (event.event) {
|
||||||
return Response.json(
|
|
||||||
{ success: false, errors: payload.error.errors },
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (payload.data.event) {
|
|
||||||
case "frame_added":
|
case "frame_added":
|
||||||
if (payload.data.notificationDetails) {
|
if (event.notificationDetails) {
|
||||||
await setUserNotificationDetails(fid, payload.data.notificationDetails);
|
await setUserNotificationDetails(fid, event.notificationDetails);
|
||||||
await sendFrameNotification({
|
await sendFrameNotification({
|
||||||
fid,
|
fid,
|
||||||
title: "Welcome to Frames v2",
|
title: "Welcome to Frames v2",
|
||||||
@ -59,7 +64,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case "notifications_enabled":
|
case "notifications_enabled":
|
||||||
await setUserNotificationDetails(fid, payload.data.notificationDetails);
|
await setUserNotificationDetails(fid, event.notificationDetails);
|
||||||
await sendFrameNotification({
|
await sendFrameNotification({
|
||||||
fid,
|
fid,
|
||||||
title: "Ding ding ding",
|
title: "Ding ding ding",
|
||||||
|
|||||||
@ -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<VerifyJsonFarcasterSignatureResult> {
|
|
||||||
// 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 };
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
const apiKey = process.env.NEYNAR_API_KEY || "";
|
|
||||||
|
|
||||||
export async function isSignerValid({
|
|
||||||
fid,
|
|
||||||
signerPublicKey,
|
|
||||||
}: {
|
|
||||||
fid: number;
|
|
||||||
signerPublicKey: string;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
26
yarn.lock
26
yarn.lock
@ -105,20 +105,28 @@
|
|||||||
ethereum-cryptography "^2.0.0"
|
ethereum-cryptography "^2.0.0"
|
||||||
micro-ftch "^0.3.1"
|
micro-ftch "^0.3.1"
|
||||||
|
|
||||||
"@farcaster/frame-core@^0.0.13":
|
"@farcaster/frame-core@^0.0.15":
|
||||||
version "0.0.13"
|
version "0.0.15"
|
||||||
resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.13.tgz#cd4739488a7492f9b4f7b302f0220e8955b3d4aa"
|
resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.15.tgz#05a4ed6f7c0d43d2f41d13714fb8b13419068a11"
|
||||||
integrity sha512-PXs2SJnt8GAKw4JspfBSWLKfvx1mGiWGZ8aB46ehMQMUtsN7k5gzSIdgWLpN47OeVEsi1X86fW6i8YkoL6L/nQ==
|
integrity sha512-WQfAEqyQAz3EzEdfqAMV7s2VMIYBGWz0Qt5CUUkmSelvv0a+8A61YmBnpemCi3NEwWzEJBTc/IxzQ29w2axPBg==
|
||||||
dependencies:
|
dependencies:
|
||||||
ox "^0.4.0"
|
ox "^0.4.0"
|
||||||
zod "^3.23.8"
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@farcaster/frame-sdk@^0.0.14":
|
"@farcaster/frame-node@^0.0.3":
|
||||||
version "0.0.14"
|
version "0.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.14.tgz#427a9156263e8bab5096af8751ed3f6bbcaef1fa"
|
resolved "https://registry.yarnpkg.com/@farcaster/frame-node/-/frame-node-0.0.3.tgz#4502694fef336cefd56b1aa5642128844ac62668"
|
||||||
integrity sha512-FPQn1LeEQbswoLpYnebrFv+UWFKV/thUX6UEn3LGWZTZyrYFPbtvJA6UHKJd8wgvU2o+/RJ5+P7tuxFcXLjHBg==
|
integrity sha512-ReZvQozIXn1pwAn56/5ms54Gw+jwWP6TW+rmlvaX4hDi7pJATnHJvwZ/nZw8yj2GsWPNr2DxUJlC0Zt0UORN/Q==
|
||||||
dependencies:
|
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"
|
comlink "^4.4.2"
|
||||||
eventemitter3 "^5.0.1"
|
eventemitter3 "^5.0.1"
|
||||||
ox "^0.4.0"
|
ox "^0.4.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user