From 96a5831a05952712f2559a49a4376caaebf7fcf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 14 Jan 2025 14:22:10 +0100 Subject: [PATCH 1/4] feat: view profile dialog --- .changeset/clean-dragons-return.md | 5 + ...frame-app-debugger-view-profile-dialog.tsx | 116 ++++++++++++++++++ .../app/components/frame-app-debugger.tsx | 19 +++ .../app/farcaster/user/[fid]/route.ts | 44 +++++++ 4 files changed, 184 insertions(+) create mode 100644 .changeset/clean-dragons-return.md create mode 100644 packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx create mode 100644 packages/debugger/app/farcaster/user/[fid]/route.ts diff --git a/.changeset/clean-dragons-return.md b/.changeset/clean-dragons-return.md new file mode 100644 index 000000000..9e08bda4c --- /dev/null +++ b/.changeset/clean-dragons-return.md @@ -0,0 +1,5 @@ +--- +"@frames.js/debugger": patch +--- + +feat: view profile dialog diff --git a/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx b/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx new file mode 100644 index 000000000..87a225885 --- /dev/null +++ b/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import { + Dialog, + DialogContent, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { useQuery } from "@tanstack/react-query"; +import { z } from "zod"; +import { Loader2Icon, TriangleAlertIcon } from "lucide-react"; +import Image from "next/image"; + +type UserDetails = { + username: string; + pfp_url: string; + profile: { + bio: { + text: string; + }; + }; + follower_count: number; + following_count: number; +}; + +type FrameAppDebuggerViewProfileDialogProps = { + fid: number; + onDismiss: () => void; +}; + +const UserDetailsSchema = z.object({ + username: z.string(), + pfp_url: z.string().url(), + profile: z.object({ + bio: z.object({ + text: z.string(), + }), + }), + follower_count: z.number().int(), + following_count: z.number().int(), +}); + +async function fetchUser(fid: number): Promise { + const response = await fetch(`/farcaster/user/${fid}`); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + + return UserDetailsSchema.parse(data); +} + +export function FrameAppDebuggerViewProfileDialog({ + fid, + onDismiss, +}: FrameAppDebuggerViewProfileDialogProps) { + const query = useQuery({ + queryKey: ["user", fid], + queryFn: () => fetchUser(fid), + }); + + return ( + + + Profile Details + {query.isLoading && ( + + + + )} + {query.isError && ( + + + Unexpected error occurred + + )} + {query.isSuccess && ( + +
+ {query.data.username} +
+ {query.data.username} +
+
+ {formatCount(query.data.follower_count)}{" "} + followers +
+
+ {formatCount(query.data.following_count)}{" "} + following +
+
+ {query.data.profile.bio.text} +
+ )} +
+
+ ); +} + +function formatCount(count: number): string { + if (count < 1000) { + return count.toString(); + } else if (count >= 1000 && count < 1000000) { + return (count / 1000).toFixed(1) + "K"; + } + + return (count / 1000000).toFixed(1) + "M"; +} diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index 6f710b042..8c2bc7e5c 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -30,6 +30,7 @@ import type { EIP6963ProviderInfo } from "@farcaster/frame-sdk"; import { z } from "zod"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { useCopyToClipboard } from "../hooks/useCopyToClipboad"; +import { FrameAppDebuggerViewProfileDialog } from "./frame-app-debugger-view-profile-dialog"; type TabValues = "events" | "console" | "notifications"; @@ -153,6 +154,7 @@ export function FrameAppDebugger({ }; } }, [toast]); + const [viewFidProfile, setViewFidProfile] = useState(null); const frameApp = useFrameAppInIframe({ debug: true, source: context.parseResult, @@ -289,6 +291,11 @@ export function FrameAppDebugger({ }); }, async onSignIn({ nonce, notBefore, expirationTime, frame }) { + console.info("sdk.actions.signIn() called", { + nonce, + notBefore, + expirationTime, + }); let abortTimeout: NodeJS.Timeout | undefined; try { @@ -373,6 +380,10 @@ export function FrameAppDebugger({ setFarcasterSignInAbortControllerURL(null); } }, + async onViewProfile(params) { + console.info("sdk.actions.viewProfile() called", params); + setViewFidProfile(params.fid); + }, }); return ( @@ -552,6 +563,14 @@ export function FrameAppDebugger({ ) : null} + {viewFidProfile !== null && ( + { + setViewFidProfile(null); + }} + /> + )} ); } diff --git a/packages/debugger/app/farcaster/user/[fid]/route.ts b/packages/debugger/app/farcaster/user/[fid]/route.ts new file mode 100644 index 000000000..f67f9e967 --- /dev/null +++ b/packages/debugger/app/farcaster/user/[fid]/route.ts @@ -0,0 +1,44 @@ +import type { NextRequest } from "next/server"; +import { z } from "zod"; + +const validator = z.object({ + fid: z.coerce.number().int().positive(), +}); + +export async function GET( + _: NextRequest, + { params }: { params: { fid: string } } +) { + try { + const { fid } = validator.parse(params); + + const url = new URL("https://api.neynar.com/v2/farcaster/user/bulk"); + + url.searchParams.set("fids", fid.toString()); + + const response = await fetch(url, { + headers: { + accept: "application/json", + "x-api-key": "NEYNAR_FRAMES_JS", + }, + }); + + if (!response.ok) { + if (response.status === 404) { + return Response.json({ error: "User not found" }, { status: 404 }); + } + + throw new Error(`Unexpected response: ${response.status}`); + } + + const data = await response.json(); + + return Response.json(data.users[0]); + } catch (e) { + if (e instanceof z.ZodError) { + return Response.json({ error: e.errors }, { status: 400 }); + } + + throw e; + } +} From 33ad0445392ee93045505ec89e4780bc412f342f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 15 Jan 2025 13:10:14 +0100 Subject: [PATCH 2/4] fix: image styling --- .../frame-app-debugger-view-profile-dialog.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx b/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx index 87a225885..437cc40b8 100644 --- a/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx +++ b/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @next/next/no-img-element */ import React from "react"; import { Dialog, @@ -78,13 +79,14 @@ export function FrameAppDebuggerViewProfileDialog({ {query.isSuccess && (
- {query.data.username} +
+ {query.data.username} +
{query.data.username}
From 86e03b92fbd2812c2699fb7d992ada5057270c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasni=C4=8D=C3=A1k?= Date: Thu, 23 Jan 2025 13:14:38 +0100 Subject: [PATCH 3/4] feat: support app key signatures (#536) * feat: support app key signatures --- .changeset/rich-bags-begin.md | 6 + packages/frames.js/package.json | 16 +- .../frames.js/src/farcaster-v2/es25519.ts | 8 + packages/frames.js/src/farcaster-v2/events.ts | 27 +- .../src/farcaster-v2/json-signature.test.ts | 244 +++++++++++++++--- .../src/farcaster-v2/json-signature.ts | 191 +++++++++++--- packages/frames.js/src/farcaster-v2/types.ts | 16 ++ packages/frames.js/src/farcaster-v2/verify.ts | 131 ++++++++++ packages/frames.js/src/lib/base64url.test.ts | 12 +- packages/frames.js/src/lib/base64url.ts | 8 +- yarn.lock | 30 +-- 11 files changed, 564 insertions(+), 125 deletions(-) create mode 100644 .changeset/rich-bags-begin.md create mode 100644 packages/frames.js/src/farcaster-v2/es25519.ts create mode 100644 packages/frames.js/src/farcaster-v2/verify.ts diff --git a/.changeset/rich-bags-begin.md b/.changeset/rich-bags-begin.md new file mode 100644 index 000000000..490f6c6c3 --- /dev/null +++ b/.changeset/rich-bags-begin.md @@ -0,0 +1,6 @@ +--- +"frames.js": patch +"@frames.js/debugger": patch +--- + +feat: support for app key signatures diff --git a/packages/frames.js/package.json b/packages/frames.js/package.json index bd62cac21..caf796d12 100644 --- a/packages/frames.js/package.json +++ b/packages/frames.js/package.json @@ -187,6 +187,16 @@ "default": "./dist/farcaster-v2/types.cjs" } }, + "./farcaster-v2/verify": { + "import": { + "types": "./dist/farcaster-v2/verify.d.ts", + "default": "./dist/farcaster-v2/verify.js" + }, + "require": { + "types": "./dist/farcaster-v2/verify.d.cts", + "default": "./dist/farcaster-v2/verify.cjs" + } + }, "./core": { "import": { "types": "./dist/core/index.d.ts", @@ -420,12 +430,14 @@ }, "dependencies": { "@farcaster/frame-core": "^0.0.24", - "@farcaster/frame-node": "^0.0.13", + "@noble/ed25519": "^2.2.3", + "@noble/hashes": "^1.7.1", "@vercel/og": "^0.6.3", "cheerio": "^1.0.0-rc.12", + "ox": "^0.4.4", "protobufjs": "^7.2.6", - "viem": "^2.7.8", "type-fest": "^4.28.1", + "viem": "^2.7.8", "zod": "^3.24.1" } } diff --git a/packages/frames.js/src/farcaster-v2/es25519.ts b/packages/frames.js/src/farcaster-v2/es25519.ts new file mode 100644 index 000000000..4c775d646 --- /dev/null +++ b/packages/frames.js/src/farcaster-v2/es25519.ts @@ -0,0 +1,8 @@ +import { sha512 } from "@noble/hashes/sha512"; +import { etc, getPublicKey, sign, verify } from "@noble/ed25519"; + +if (!etc.sha512Sync) { + etc.sha512Sync = (...m: Uint8Array[]) => sha512(etc.concatBytes(...m)); +} + +export { getPublicKey, sign, verify }; diff --git a/packages/frames.js/src/farcaster-v2/events.ts b/packages/frames.js/src/farcaster-v2/events.ts index 381a5dce0..ff8206108 100644 --- a/packages/frames.js/src/farcaster-v2/events.ts +++ b/packages/frames.js/src/farcaster-v2/events.ts @@ -3,11 +3,9 @@ import type { EncodedJsonFarcasterSignatureSchema, } from "@farcaster/frame-core"; import { serverEventSchema } from "@farcaster/frame-core"; -import { - createJsonFarcasterSignature, - hexToBytes, -} from "@farcaster/frame-node"; -import type { Hex } from "viem"; +import { bytesToHex, type Hex } from "viem"; +import { sign, signMessageWithAppKey } from "./json-signature"; +import { getPublicKey } from "./es25519"; export class InvalidWebhookResponseError extends Error { constructor( @@ -22,7 +20,7 @@ export type { FrameServerEvent }; type SendEventOptions = { /** - * App private key + * Private app key (signer private key) */ privateKey: Hex | Uint8Array; fid: number; @@ -36,13 +34,16 @@ export async function sendEvent( event: FrameServerEvent, { privateKey, fid, webhookUrl }: SendEventOptions ): Promise { + const appKey = bytesToHex(getPublicKey(privateKey)); const payload = serverEventSchema.parse(event); - const signature = createJsonFarcasterSignature({ + const signature = await sign({ fid, - payload: Buffer.from(JSON.stringify(payload)), - privateKey: - typeof privateKey === "string" ? hexToBytes(privateKey) : privateKey, - type: "app_key", + payload, + signer: { + type: "app_key", + appKey, + }, + signMessage: signMessageWithAppKey(privateKey), }); const response = await fetch(webhookUrl, { @@ -52,11 +53,11 @@ export async function sendEvent( "Content-Type": "application/json", }, body: JSON.stringify( - signature satisfies EncodedJsonFarcasterSignatureSchema + signature.json satisfies EncodedJsonFarcasterSignatureSchema ), }); - if (response.status >= 200 && response.status < 300) { + if (response.ok) { return; } diff --git a/packages/frames.js/src/farcaster-v2/json-signature.test.ts b/packages/frames.js/src/farcaster-v2/json-signature.test.ts index 98d45793b..f5cd339d7 100644 --- a/packages/frames.js/src/farcaster-v2/json-signature.test.ts +++ b/packages/frames.js/src/farcaster-v2/json-signature.test.ts @@ -1,4 +1,9 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment -- for expect.any() */ +import { webcrypto } from "node:crypto"; +import * as ed25519 from "@noble/ed25519"; +import { sha512 } from "@noble/hashes/sha512"; +import type { Hex } from "viem"; +import { bytesToHex, hexToBytes } from "viem"; import { sign, verify, @@ -8,10 +13,23 @@ import { encodeSignature, decodeHeader, decodePayload, - decodeSignature, + decodeCustodyTypeSignature, + decodeAppKeyTypeSignature, constructJSONFarcasterSignatureAccountAssociationPaylod, + signMessageWithAppKey, } from "./json-signature"; +// polyfill for node 18 so we can use randomPrivateKey() +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- not true in node 18 +if (!globalThis.crypto) { + // @ts-expect-error -- this is polyfill + globalThis.crypto = webcrypto; +} + +process.env.NEYNAR_API_KEY = "NEYNAR_FRAMES_JS"; + +ed25519.etc.sha512Sync = (...m) => sha512(ed25519.etc.concatBytes(...m)); + const fcDemoSignature = { header: "eyJmaWQiOjM2MjEsInR5cGUiOiJjdXN0b2R5Iiwia2V5IjoiMHgyY2Q4NWEwOTMyNjFmNTkyNzA4MDRBNkVBNjk3Q2VBNENlQkVjYWZFIn0", @@ -33,58 +51,144 @@ const framesJsDemoSignature = { const framesJsDemoCompactSignature = "eyJmaWQiOjM0MTc5NCwidHlwZSI6ImN1c3RvZHkiLCJrZXkiOiIweDc4Mzk3RDlEMTg1RDNhNTdEMDEyMTNDQmUzRWMxRWJBQzNFRWM3N2QifQ.eyJkb21haW4iOiJmcmFtZXNqcy5vcmcifQ.MHgwOWExNWMyZDQ3ZDk0NTM5NWJjYTJlNGQzNDg3MzYxMGUyNGZiMDFjMzc0NTUzYTJmOTM2NjM3YjU4YTA5NzdjNzAxOWZiYzljNGUxY2U5ZmJjOGMzNWVjYTllNzViMTM5Zjg3ZGQyNTBlMzhkMjBmM2YyZmEyNDk2MDQ1NGExMjFi"; -const signatures = [framesJsDemoSignature, fcDemoSignature]; +const custodySignatures = [framesJsDemoSignature, fcDemoSignature]; + +const dummyAppKeySignature = { + header: + "eyJmaWQiOjM0MTc5NCwidHlwZSI6ImFwcF9rZXkiLCJrZXkiOiIweGJkZGVhNDQ2ODUxZDYwZjQ4OTAxNjU1NDc4YTIwNTQ3MmNjOTJmNGUwMzdiNTIzNmE1YzVhYmZjMWI4ZTA5MWIifQ", + payload: "eyJ0ZXN0Ijp0cnVlfQ", + signature: + "Y1C9-m6EIAPDqd8-2NrSXBKrpvWKUfA3Qjy865De5yUu7MV_b1TjsQKtwqbaVv_UzFz5ghmvygVbGjhx-kbRDw", +}; + +const dummyAppKeyCompactSignature = `${dummyAppKeySignature.header}.${dummyAppKeySignature.payload}.${dummyAppKeySignature.signature}`; +const appKeySignatures = [dummyAppKeySignature]; -const compactSignatures = [ +const custodyCompactSignatures = [ framesJsDemoCompactSignature, fcDemoCompactSignature, ]; +const appKeyCompactSignatures = [dummyAppKeyCompactSignature]; describe("verifyCompact", () => { - it.each(compactSignatures)("verifies valid message", async (signature) => { - await expect(verifyCompact(signature)).resolves.toBe(true); + describe("custody", () => { + it.each(custodyCompactSignatures)( + "verifies valid message", + async (signature) => { + await expect(verifyCompact(signature)).resolves.toBe(true); + } + ); + }); + + describe("app_key", () => { + it.each(appKeyCompactSignatures)( + "verifies valid message", + async (signature) => { + await expect(verifyCompact(signature)).resolves.toBe(true); + } + ); }); }); describe("verify", () => { - it.each(signatures)("verifies valid message", async (signature) => { - await expect(verify(signature)).resolves.toBe(true); + describe("custody", () => { + it.each(custodySignatures)("verifies valid message", async (signature) => { + await expect(verify(signature)).resolves.toBe(true); + }); + }); + + describe("app_key", () => { + it.each(appKeySignatures)("verifies valid message", async (signature) => { + await expect(verify(signature)).resolves.toBe(true); + }); }); }); describe("sign", () => { - it("signs any payload", async () => { - const signature = await sign({ - fid: 1, - payload: { test: true }, - signer: { - type: "custody", - custodyAddress: "0x1234567890abcdef1234567890abcdef12345678", - }, - signMessage: (message) => { - expect(typeof message === "string").toBe(true); - expect(message.length).toBeGreaterThan(0); + describe("custody", () => { + it("signs any payload", async () => { + const signature = await sign({ + fid: 1, + payload: { test: true }, + signer: { + type: "custody", + custodyAddress: "0x1234567890abcdef1234567890abcdef12345678", + }, + signMessage: (message) => { + expect(typeof message === "string").toBe(true); + expect(message.length).toBeGreaterThan(0); - return Promise.resolve("0x0000000"); - }, - }); + return Promise.resolve("0x0000000"); + }, + }); - expect(signature).toMatchObject({ - compact: expect.any(String), - json: { - header: expect.any(String), - payload: expect.any(String), - signature: expect.any(String), - }, + expect(signature).toMatchObject({ + compact: expect.any(String), + json: { + header: expect.any(String), + payload: expect.any(String), + signature: expect.any(String), + }, + }); + + expect(decodePayload(signature.json.payload)).toEqual({ test: true }); + expect(decodeHeader(signature.json.header)).toEqual({ + fid: 1, + type: "custody", + key: "0x1234567890abcdef1234567890abcdef12345678", + }); + expect(decodeCustodyTypeSignature(signature.json.signature)).toEqual( + "0x0000000" + ); }); + }); - expect(decodePayload(signature.json.payload)).toEqual({ test: true }); - expect(decodeHeader(signature.json.header)).toEqual({ - fid: 1, - type: "custody", - key: "0x1234567890abcdef1234567890abcdef12345678", + describe("app_key", () => { + it("signs any payload", async () => { + const privateKey = ed25519.utils.randomPrivateKey(); + let messageSignature: Hex = "0x"; + const signature = await sign({ + fid: 1, + payload: { test: true }, + signer: { + type: "app_key", + appKey: bytesToHex(ed25519.getPublicKey(privateKey)), + }, + signMessage: (message) => { + expect(typeof message === "string").toBe(true); + expect(message.length).toBeGreaterThan(0); + + messageSignature = bytesToHex( + ed25519.sign(Buffer.from(message, "utf-8"), privateKey) + ); + + return Promise.resolve(messageSignature); + }, + }); + + expect(signature).toMatchObject({ + compact: expect.any(String), + json: { + header: expect.any(String), + payload: expect.any(String), + signature: expect.any(String), + }, + }); + + expect(Buffer.from(signature.json.signature, "base64url")).toHaveProperty( + "byteLength", + 64 + ); + expect(decodePayload(signature.json.payload)).toEqual({ test: true }); + expect(decodeHeader(signature.json.header)).toEqual({ + fid: 1, + type: "app_key", + key: bytesToHex(ed25519.getPublicKey(privateKey)), + }); + expect(decodeAppKeyTypeSignature(signature.json.signature)).toEqual( + messageSignature + ); }); - expect(decodeSignature(signature.json.signature)).toEqual("0x0000000"); }); }); @@ -140,17 +244,77 @@ describe("decodePayload", () => { describe("encodeSignature", () => { it("encodes signature", () => { - const value = encodeSignature("0x0000000"); + const input = "0x0000000"; + const value = encodeSignature(Buffer.from("0x0000000", "utf-8")); expect(typeof value).toBe("string"); + expect(Buffer.from(input, "utf-8").toString("base64url")).toEqual(value); + }); + + it("encodes signature as Buffer", () => { + const input = hexToBytes("0x0000001"); + const value = encodeSignature(Buffer.from(input)); + + expect(Buffer.from(input).toString("base64url")).toEqual(value); + }); +}); + +describe("decodeAppKeyTypeSignature", () => { + it("decodes signature (string)", () => { + const buf = Buffer.from("0x0000000", "utf-8"); + const encodedSignature = encodeSignature(buf); + const value = decodeAppKeyTypeSignature(encodedSignature); + + expect(value).toBe(bytesToHex(buf)); + }); + + it("decodes signature (from buffer)", () => { + const input = hexToBytes("0x0000001"); + const encodedSignature = encodeSignature(Buffer.from(input)); + const value = decodeAppKeyTypeSignature(encodedSignature); + + expect(value).toBe(bytesToHex(input)); + }); +}); + +describe("decodeCustodyTypeSignature", () => { + it("decodes signature (string)", () => { + const buf = Buffer.from("0x0000000", "utf-8"); + const encodedSignature = encodeSignature(buf); + const value = decodeCustodyTypeSignature(encodedSignature); + + expect(value).toBe(buf.toString("utf-8")); + }); + + it("decodes signature (from buffer)", () => { + const input = "0x0000001"; + const encodedSignature = encodeSignature(Buffer.from(input, "utf-8")); + const value = decodeCustodyTypeSignature(encodedSignature); + + expect(value).toBe(input); }); }); -describe("decodeSignature", () => { - it("decodes signature", () => { - const encodedSignature = encodeSignature("0x0000000"); - const value = decodeSignature(encodedSignature); +describe("signMessageWithAppKey", () => { + it("signs any payload", async () => { + const privateKey = ed25519.utils.randomPrivateKey(); + const signature = await sign({ + fid: 1, + payload: { test: true }, + signer: { + type: "app_key", + appKey: bytesToHex(ed25519.getPublicKey(privateKey)), + }, + signMessage: signMessageWithAppKey(privateKey), + }); - expect(value).toBe("0x0000000"); + expect(signature).toMatchObject({ + compact: expect.any(String), + json: { + header: expect.any(String), + payload: expect.any(String), + signature: expect.any(String), + }, + }); }); }); diff --git a/packages/frames.js/src/farcaster-v2/json-signature.ts b/packages/frames.js/src/farcaster-v2/json-signature.ts index d0e88ea4d..578fdd1d1 100644 --- a/packages/frames.js/src/farcaster-v2/json-signature.ts +++ b/packages/frames.js/src/farcaster-v2/json-signature.ts @@ -1,7 +1,16 @@ -import { createPublicClient, http, parseAbi } from "viem"; +import { + bytesToHex, + createPublicClient, + hexToBytes, + http, + parseAbi, +} from "viem"; import { optimism } from "viem/chains"; import type { JsonObject } from "../core/types"; import { base64urlDecode, base64urlEncode } from "../lib/base64url"; +import { sign as signEd25519, verify as verifyEd25519 } from "./es25519"; +import { verifyAppKeyWithNeynar } from "./verify"; +import type { SignMessageFunction, VerifyAppKeyFunction } from "./types"; export class InvalidJFSHeaderError extends Error {} @@ -9,7 +18,11 @@ export class InvalidJFSPayloadError extends Error {} export class InvalidJFSCompactSignatureError extends Error {} -export class InvalidJFSSignatureError extends Error {} +export class InvalidJFSSignatureError extends Error { + constructor(public cause: unknown) { + super(); + } +} export type JSONFarcasterSignatureHeader = { fid: number; @@ -58,7 +71,7 @@ type GenerateJSONFarcasterSignatureInput = { fid: number; signer: JSONFarcasterSignatureSigner; payload: JsonObject; - signMessage: (message: string) => Promise<`0x${string}`>; + signMessage: SignMessageFunction; }; export type SignResult = { @@ -71,6 +84,7 @@ export type SignResult = { * * @example * ```ts + * // signing domain for frame manifest * const signature = await sign({ * fid: 1, * signer: { @@ -94,14 +108,30 @@ export async function sign( const signature = await input.signMessage( `${encodedHeader}.${encodedPayload}` ); - const encodedSignature = encodeSignature(signature); + let base64urlEncodedSignature; + + /** + * Farcaster seems to encode signatures differently based on signer type. + * + * For app_key it uses signature data as bytes, so encoding raw data to base64url + * For custody it uses signature as hex string, which is then encoded to base64url + */ + if (input.signer.type === "app_key") { + base64urlEncodedSignature = encodeSignature( + Buffer.from(hexToBytes(signature)) + ); + } else { + base64urlEncodedSignature = encodeSignature( + Buffer.from(signature, "utf-8") + ); + } return { - compact: `${encodedHeader}.${encodedPayload}.${encodedSignature}`, + compact: `${encodedHeader}.${encodedPayload}.${base64urlEncodedSignature}`, json: { header: encodedHeader, payload: encodedPayload, - signature: encodedSignature, + signature: base64urlEncodedSignature, }, }; } @@ -115,7 +145,10 @@ export async function sign( * ``` */ export async function verifyCompact( - compactSignature: string + compactSignature: string, + options?: { + verifyAppKey?: VerifyAppKeyFunction; + } ): Promise { const [encodedHeader, encodedPayload, encodedSignature] = compactSignature.split("."); @@ -124,15 +157,18 @@ export async function verifyCompact( throw new InvalidJFSCompactSignatureError(); } - return verify({ - header: encodedHeader, - payload: encodedPayload, - signature: encodedSignature, - }); + return verify( + { + header: encodedHeader, + payload: encodedPayload, + signature: encodedSignature, + }, + options + ); } /** - * Verifies JSON Farcaster Signature + * Verifies JSON Farcaster Signature either signed using custody address or app key * * @example * ```ts @@ -142,17 +178,54 @@ export async function verifyCompact( * signature: "encoded signature", * }); * ``` + * + * @example + * ```ts + * // use custom hub url + * const isValid = await verify({ + * header: "encoded header", + * payload: "encoded payload", + * signature: "encoded signature", + * }, { + * verifyAppKey: verifyAppKeyWithNeynar({ apiKey: 'api key', hubUrl: "https://hub-api.neynar.com" }), + * }); + * ``` */ export async function verify( - signatureObject: JSONFarcasterSignatureEncoded + signatureObject: JSONFarcasterSignatureEncoded, + options: { + verifyAppKey?: VerifyAppKeyFunction; + } = {} ): Promise { const decodedHeader = decodeHeader(signatureObject.header); - if (decodedHeader.type !== "custody") { - throw new InvalidJFSHeaderError("Only custody signatures are supported"); + if (decodedHeader.type === "app_key") { + const signature = base64urlDecode(signatureObject.signature); + const signedInput = Buffer.from( + `${signatureObject.header}.${signatureObject.payload}` + ); + const appKey = hexToBytes(decodedHeader.key); + const isValid = verifyEd25519(signature, signedInput, appKey); + + if (!isValid) { + return false; + } + + const verifyAppKey = options.verifyAppKey ?? verifyAppKeyWithNeynar(); + + const appKeyResult = await verifyAppKey( + decodedHeader.fid, + decodedHeader.key + ); + + if (!appKeyResult.valid) { + return false; + } + + return true; } - const signature = decodeSignature(signatureObject.signature); + const signature = decodeCustodyTypeSignature(signatureObject.signature); const publicClient = createPublicClient({ chain: optimism, transport: http(), @@ -187,7 +260,10 @@ type JSONFarcasterSignatureSigner = } | { type: "app_key"; - appKey: string; + /** + * Farcaster signer public key + */ + appKey: `0x${string}`; }; export function encodeHeader( @@ -195,11 +271,14 @@ export function encodeHeader( signer: JSONFarcasterSignatureSigner ): string { return base64urlEncode( - JSON.stringify({ - fid, - type: signer.type, - key: signer.type === "custody" ? signer.custodyAddress : signer.appKey, - }) + Buffer.from( + JSON.stringify({ + fid, + type: signer.type, + key: signer.type === "custody" ? signer.custodyAddress : signer.appKey, + }), + "utf-8" + ) ); } @@ -208,7 +287,7 @@ export function decodeHeader( ): JSONFarcasterSignatureHeader { try { const decodedHeader = base64urlDecode(encodedHeader); - const value: unknown = JSON.parse(decodedHeader); + const value: unknown = JSON.parse(decodedHeader.toString("utf-8")); const header: JSONFarcasterSignatureHeader = { fid: 0, type: "custody", @@ -261,13 +340,13 @@ export function decodeHeader( } export function encodePayload(data: JsonObject): string { - return base64urlEncode(JSON.stringify(data)); + return base64urlEncode(Buffer.from(JSON.stringify(data), "utf-8")); } export function decodePayload(encodedPayload: string): JsonObject { try { const decodedPayload = base64urlDecode(encodedPayload); - const value: unknown = JSON.parse(decodedPayload); + const value: unknown = JSON.parse(decodedPayload.toString("utf-8")); if (typeof value !== "object") { throw new InvalidJFSPayloadError(); @@ -287,24 +366,62 @@ export function decodePayload(encodedPayload: string): JsonObject { } } -export function encodeSignature(signature: `0x${string}`): string { +export function encodeSignature(signature: Buffer): string { return base64urlEncode(signature); } -export function decodeSignature(signature: string): `0x${string}` { +export function decodeAppKeyTypeSignature(signature: string): `0x${string}` { try { - const signatureHash = base64urlDecode(signature); + return bytesToHex(base64urlDecode(signature)); + } catch (e) { + throw new InvalidJFSSignatureError(e); + } +} - if (!signatureHash.startsWith("0x")) { - throw new InvalidJFSSignatureError(); - } +export function decodeCustodyTypeSignature(signature: string): `0x${string}` { + try { + const decoded = base64urlDecode(signature).toString("utf-8"); - return signatureHash as `0x${string}`; - } catch (e) { - if (e instanceof InvalidJFSSignatureError) { - throw e; + if (!decoded.startsWith("0x")) { + throw new Error("Invalid signature, must contain hex text"); } - throw new InvalidJFSSignatureError(); + return decoded as `0x${string}`; + } catch (e) { + throw new InvalidJFSSignatureError(e); } } + +/** + * Signs message using app key + * + * @example + * ```ts + * await sign({ + * fid: 1, + * signer: { + * type: "app_key", + * appKey: "0x000000000000000000000000" // signer public key + * }, + * payload: { + * any: 10, + * }, + * signMessage: signMessageWithAppKey( + * // signer private key + * "0x000000000000000000000000000000" + * ), + * }) + * ``` + */ +export function signMessageWithAppKey( + privateKey: `0x${string}` | Uint8Array +): SignMessageFunction { + return (message) => { + const signature = signEd25519( + Buffer.from(message, "utf-8"), + typeof privateKey === "string" ? hexToBytes(privateKey) : privateKey + ); + + return Promise.resolve(bytesToHex(signature)); + }; +} diff --git a/packages/frames.js/src/farcaster-v2/types.ts b/packages/frames.js/src/farcaster-v2/types.ts index 8a0962ffd..574e81572 100644 --- a/packages/frames.js/src/farcaster-v2/types.ts +++ b/packages/frames.js/src/farcaster-v2/types.ts @@ -10,3 +10,19 @@ export type FarcasterManifest = z.infer; export type Frame = FrameEmbedNext; export type PartialFarcasterManifest = PartialDeep; + +export type VerifyAppKeyFunctionResult = + | { + readonly valid: false; + } + | { + readonly valid: true; + readonly appFid: number; + }; + +export type VerifyAppKeyFunction = ( + fid: number, + appKey: string +) => Promise; + +export type SignMessageFunction = (message: string) => Promise<`0x${string}`>; diff --git a/packages/frames.js/src/farcaster-v2/verify.ts b/packages/frames.js/src/farcaster-v2/verify.ts new file mode 100644 index 000000000..2b5c32105 --- /dev/null +++ b/packages/frames.js/src/farcaster-v2/verify.ts @@ -0,0 +1,131 @@ +import { AbiParameters } from "ox"; +import { z } from "zod"; +import type { VerifyAppKeyFunction } from "./types"; + +class VerifyAppKeyWithNeynarError extends Error { + constructor( + message: string, + public cause?: unknown + ) { + super(message); + } +} + +type VerifyAppKeyWithNeynarOptions = { + /** + * @defaultValue process.env.NEYNAR_API_KEY + */ + apiKey?: string; + /** + * @defaultValue "https://hub-api.neynar.com" + */ + hubUrl?: string; +}; + +export function verifyAppKeyWithNeynar({ + apiKey = process.env.NEYNAR_API_KEY, + hubUrl = "https://hub-api.neynar.com", +}: VerifyAppKeyWithNeynarOptions = {}): VerifyAppKeyFunction { + if (!apiKey) { + throw new Error( + "verifyAppKeyWithNeynar requires appKey to be passed in or set as an environment variable NEYNAR_API_KEY" + ); + } + + const verifier = createVerifyAppKeyWithHub(hubUrl, { + headers: { + "x-api-key": apiKey, + }, + cache: "no-cache", + }); + + return verifier; +} + +export const signedKeyRequestAbi = [ + { + components: [ + { + name: "requestFid", + type: "uint256", + }, + { + name: "requestSigner", + type: "address", + }, + { + name: "signature", + type: "bytes", + }, + { + name: "deadline", + type: "uint256", + }, + ], + name: "SignedKeyRequest", + type: "tuple", + }, +] as const; + +const hubResponseSchema = z.object({ + events: z.array( + z.object({ + signerEventBody: z.object({ + key: z.string(), + metadata: z.string(), + }), + }) + ), +}); + +function createVerifyAppKeyWithHub( + hubUrl: string, + requestOptions: RequestInit +): VerifyAppKeyFunction { + return async (fid, appKey) => { + const url = new URL("/v1/onChainSignersByFid", hubUrl); + url.searchParams.append("fid", fid.toString()); + + const response = await fetch(url, requestOptions); + + if (response.status !== 200) { + throw new VerifyAppKeyWithNeynarError( + "Error fetching from Hub API, non-200 status code received", + await response.text() + ); + } + + const parsedResponse = hubResponseSchema.safeParse(await response.json()); + + if (parsedResponse.error) { + throw new VerifyAppKeyWithNeynarError( + "Error parsing Hub response", + parsedResponse.error + ); + } + + const appKeyLower = appKey.toLowerCase(); + + const signerEvent = parsedResponse.data.events.find( + (event) => event.signerEventBody.key.toLowerCase() === appKeyLower + ); + + if (!signerEvent) { + return { valid: false }; + } + + const decoded = AbiParameters.decode( + signedKeyRequestAbi, + Buffer.from(signerEvent.signerEventBody.metadata, "base64") + ); + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- on type level this returns a tuple + if (decoded.length !== 1) { + throw new VerifyAppKeyWithNeynarError("Error decoding metadata"); + } + + const appFid = Number(decoded[0].requestFid); + + return { valid: true, appFid }; + }; +} diff --git a/packages/frames.js/src/lib/base64url.test.ts b/packages/frames.js/src/lib/base64url.test.ts index 030fb9716..5770d2e0e 100644 --- a/packages/frames.js/src/lib/base64url.test.ts +++ b/packages/frames.js/src/lib/base64url.test.ts @@ -2,20 +2,16 @@ import { base64urlDecode, base64urlEncode } from "./base64url"; describe("base64urlEncode", () => { it('works the same as native buffer toString("base64url")', () => { - const data = "hello world"; + const data = Buffer.from("hello world"); - expect(base64urlEncode(data)).toEqual( - Buffer.from(data, "utf-8").toString("base64url") - ); + expect(base64urlEncode(data)).toEqual(data.toString("base64url")); }); }); describe("base64urlDecode", () => { it('decodes the same as native buffer from("base64url")', () => { - const data = base64urlEncode("hello world"); + const data = base64urlEncode(Buffer.from("hello world")); - expect(base64urlDecode(data)).toEqual( - Buffer.from(data, "base64url").toString("utf-8") - ); + expect(base64urlDecode(data)).toEqual(Buffer.from("hello world")); }); }); diff --git a/packages/frames.js/src/lib/base64url.ts b/packages/frames.js/src/lib/base64url.ts index 9ad308882..582e2b39c 100644 --- a/packages/frames.js/src/lib/base64url.ts +++ b/packages/frames.js/src/lib/base64url.ts @@ -1,13 +1,13 @@ -export function base64urlEncode(data: string): string { +export function base64urlEncode(data: Buffer): string { // we could use .toString('base64url') on buffer, but that throws in browser - return Buffer.from(data, "utf-8") + return data .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); } -export function base64urlDecode(encodedData: string): string { +export function base64urlDecode(encodedData: string): Buffer { const encodedChunks = encodedData.length % 4; const base64 = encodedData .replace(/-/g, "+") @@ -15,5 +15,5 @@ export function base64urlDecode(encodedData: string): string { .padEnd(encodedData.length + Math.max(0, 4 - encodedChunks), "="); // we could use base64url on buffer, but that throws in browser - return Buffer.from(base64, "base64").toString("utf-8"); + return Buffer.from(base64, "base64"); } diff --git a/yarn.lock b/yarn.lock index 4829ea586..0ca28cefc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2452,16 +2452,6 @@ "@farcaster/frame-core" "0.0.24" ox "^0.4.4" -"@farcaster/frame-node@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@farcaster/frame-node/-/frame-node-0.0.13.tgz#ba9d37358589b263aa9566cf1763b1eb528672e6" - integrity sha512-99rhLSpyhKKnWFh8eWx8N5tTYEToFkIzg9r5lRm2bvfsZaT0zXr/cA+G9pWDFZyodfoyS/cLHS46Qsm8P52uTg== - dependencies: - "@farcaster/frame-core" "0.0.24" - "@noble/curves" "^1.7.0" - ox "^0.4.4" - zod "^3.24.1" - "@farcaster/frame-sdk@^0.0.26": version "0.0.26" resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.26.tgz#2cf5c5e9e8ecdbdbc244e55f41129fc1caa9b88c" @@ -3576,13 +3566,6 @@ dependencies: "@noble/hashes" "1.6.0" -"@noble/curves@^1.7.0": - version "1.8.0" - resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.8.0.tgz#fe035a23959e6aeadf695851b51a87465b5ba8f7" - integrity sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ== - dependencies: - "@noble/hashes" "1.7.0" - "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" @@ -3595,6 +3578,11 @@ resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-2.0.0.tgz#5964c8190a4b4b804985717ca566113b93379e43" integrity sha512-/extjhkwFupyopDrt80OMWKdLgP429qLZj+z6sYJz90rF2Iz0gjZh2ArMKPImUl13Kx+0EXI2hN9T/KJV0/Zng== +"@noble/ed25519@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-2.2.3.tgz#e189810490302b076e17895b667a06cbe54339f4" + integrity sha512-iHV8eI2mRcUmOx159QNrU8vTpQ/Xm70yJ2cTk3Trc86++02usfqFoNl6x0p3JN81ZDS/1gx6xiK0OwrgqCT43g== + "@noble/hashes@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" @@ -3620,10 +3608,10 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== -"@noble/hashes@1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.0.tgz#5d9e33af2c7d04fee35de1519b80c958b2e35e39" - integrity sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w== +"@noble/hashes@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== "@noble/secp256k1@1.7.1": version "1.7.1" From c0c651e0f59ef9ed91cf14eccbfb2334543bab3c Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:22:00 +0200 Subject: [PATCH 4/4] @frames.js/debugger@0.4.2, @frames.js/render@0.5.2, frames.js@0.21.2 --- .changeset/clean-dragons-return.md | 5 ----- .changeset/rich-bags-begin.md | 6 ------ docs/CHANGELOG.md | 7 +++++++ docs/package.json | 4 ++-- packages/debugger/CHANGELOG.md | 7 +++++++ packages/debugger/package.json | 6 +++--- packages/frames.js/CHANGELOG.md | 6 ++++++ packages/frames.js/package.json | 2 +- packages/render/CHANGELOG.md | 7 +++++++ packages/render/package.json | 4 ++-- .../CHANGELOG.md | 7 +++++++ .../package.json | 6 +++--- templates/cloudflare-worker/CHANGELOG.md | 7 +++++++ templates/cloudflare-worker/package.json | 6 +++--- templates/express/CHANGELOG.md | 7 +++++++ templates/express/package.json | 6 +++--- templates/hono/CHANGELOG.md | 7 +++++++ templates/hono/package.json | 6 +++--- templates/next-starter-with-examples/CHANGELOG.md | 7 +++++++ templates/next-starter-with-examples/package.json | 6 +++--- templates/next-utils-starter/CHANGELOG.md | 7 +++++++ templates/next-utils-starter/package.json | 6 +++--- templates/next/CHANGELOG.md | 7 +++++++ templates/next/package.json | 6 +++--- templates/remix/CHANGELOG.md | 7 +++++++ templates/remix/package.json | 6 +++--- 26 files changed, 115 insertions(+), 43 deletions(-) delete mode 100644 .changeset/clean-dragons-return.md delete mode 100644 .changeset/rich-bags-begin.md diff --git a/.changeset/clean-dragons-return.md b/.changeset/clean-dragons-return.md deleted file mode 100644 index 9e08bda4c..000000000 --- a/.changeset/clean-dragons-return.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@frames.js/debugger": patch ---- - -feat: view profile dialog diff --git a/.changeset/rich-bags-begin.md b/.changeset/rich-bags-begin.md deleted file mode 100644 index 490f6c6c3..000000000 --- a/.changeset/rich-bags-begin.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"frames.js": patch -"@frames.js/debugger": patch ---- - -feat: support for app key signatures diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e8e91fad2..f98074701 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,12 @@ # docs +## 0.3.13 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.3.12 ### Patch Changes diff --git a/docs/package.json b/docs/package.json index 4379c814c..638b6b584 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "docs", - "version": "0.3.12", + "version": "0.3.13", "type": "module", "private": true, "scripts": { @@ -9,7 +9,7 @@ "preview": "vocs preview" }, "dependencies": { - "frames.js": "0.21.1", + "frames.js": "0.21.2", "lucide-react": "^0.372.0", "next": "14.1.4", "react": "^18.2.0", diff --git a/packages/debugger/CHANGELOG.md b/packages/debugger/CHANGELOG.md index 260dd2f21..990d13642 100644 --- a/packages/debugger/CHANGELOG.md +++ b/packages/debugger/CHANGELOG.md @@ -1,5 +1,12 @@ # @frames.js/debugger +## 0.4.2 + +### Patch Changes + +- 96a5831: feat: view profile dialog +- 86e03b9: feat: support for app key signatures + ## 0.4.1 ### Patch Changes diff --git a/packages/debugger/package.json b/packages/debugger/package.json index e7de2429b..bc98a4516 100644 --- a/packages/debugger/package.json +++ b/packages/debugger/package.json @@ -5,7 +5,7 @@ "frames.js", "farcaster" ], - "version": "0.4.1", + "version": "0.4.2", "bin": { "frames": "./bin/debugger.js" }, @@ -37,7 +37,7 @@ }, "devDependencies": { "@farcaster/core": "^0.15.6", - "@frames.js/render": "^0.5.1", + "@frames.js/render": "^0.5.2", "@primer/octicons-react": "^19.9.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", @@ -69,7 +69,7 @@ "console-feed": "^3.6.0", "eslint": "^8.56.0", "eslint-config-next": "^14.1.0", - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "lucide-react": "^0.408.0", "postcss": "^8", "qrcode.react": "^3.1.0", diff --git a/packages/frames.js/CHANGELOG.md b/packages/frames.js/CHANGELOG.md index 1f37c0eeb..740ead9d6 100644 --- a/packages/frames.js/CHANGELOG.md +++ b/packages/frames.js/CHANGELOG.md @@ -1,5 +1,11 @@ # frames.js +## 0.21.2 + +### Patch Changes + +- 86e03b9: feat: support for app key signatures + ## 0.21.1 ### Patch Changes diff --git a/packages/frames.js/package.json b/packages/frames.js/package.json index caf796d12..ea8110226 100644 --- a/packages/frames.js/package.json +++ b/packages/frames.js/package.json @@ -1,6 +1,6 @@ { "name": "frames.js", - "version": "0.21.1", + "version": "0.21.2", "type": "module", "main": "./dist/index.cjs", "types": "index.d.cts", diff --git a/packages/render/CHANGELOG.md b/packages/render/CHANGELOG.md index 9c36a88b7..b0d5f29b7 100644 --- a/packages/render/CHANGELOG.md +++ b/packages/render/CHANGELOG.md @@ -1,5 +1,12 @@ # @frames.js/render +## 0.5.2 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.5.1 ### Patch Changes diff --git a/packages/render/package.json b/packages/render/package.json index 1375f0e3d..c2492eb46 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -1,6 +1,6 @@ { "name": "@frames.js/render", - "version": "0.5.1", + "version": "0.5.2", "type": "module", "main": "./dist/index.cjs", "types": "index.d.cts", @@ -370,7 +370,7 @@ "@farcaster/core": "^0.15.6", "@farcaster/frame-host": "^0.0.24", "@noble/ed25519": "^2.0.0", - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "ox": "^0.4.4", "zod": "^3.24.1" } diff --git a/templates/cloudflare-worker-with-custom-images-worker/CHANGELOG.md b/templates/cloudflare-worker-with-custom-images-worker/CHANGELOG.md index d24b0f871..41123edf1 100644 --- a/templates/cloudflare-worker-with-custom-images-worker/CHANGELOG.md +++ b/templates/cloudflare-worker-with-custom-images-worker/CHANGELOG.md @@ -1,5 +1,12 @@ # template-cloudflare-worker-with-custom-images-worker +## 0.0.27 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.0.26 ### Patch Changes diff --git a/templates/cloudflare-worker-with-custom-images-worker/package.json b/templates/cloudflare-worker-with-custom-images-worker/package.json index ee64d2069..b605c8200 100644 --- a/templates/cloudflare-worker-with-custom-images-worker/package.json +++ b/templates/cloudflare-worker-with-custom-images-worker/package.json @@ -1,16 +1,16 @@ { "name": "template-cloudflare-worker-with-custom-images-worker", "description": "Simple Cloudflare Worker template with Frames.js, React and TypeScript that uses custom image worker", - "version": "0.0.26", + "version": "0.0.27", "type": "module", "private": true, "dependencies": { - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "react": "^18.2.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240320.1", - "@frames.js/debugger": "^0.4.1", + "@frames.js/debugger": "^0.4.2", "@types/node": "^18.17.0", "@types/react": "^18.2.45", "concurrently": "^8.2.2", diff --git a/templates/cloudflare-worker/CHANGELOG.md b/templates/cloudflare-worker/CHANGELOG.md index 1dfd39e4e..86e384432 100644 --- a/templates/cloudflare-worker/CHANGELOG.md +++ b/templates/cloudflare-worker/CHANGELOG.md @@ -1,5 +1,12 @@ # template-cloudflare-worker +## 0.0.38 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.0.37 ### Patch Changes diff --git a/templates/cloudflare-worker/package.json b/templates/cloudflare-worker/package.json index db19c784a..1bda813af 100644 --- a/templates/cloudflare-worker/package.json +++ b/templates/cloudflare-worker/package.json @@ -1,17 +1,17 @@ { "name": "template-cloudflare-worker", "description": "Simple Cloudflare Worker template with Frames.js, React and TypeScript", - "version": "0.0.37", + "version": "0.0.38", "type": "module", "private": true, "dependencies": { - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "react": "^18.2.0" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.1.0", "@cloudflare/workers-types": "^4.20240320.1", - "@frames.js/debugger": "^0.4.1", + "@frames.js/debugger": "^0.4.2", "@types/node": "^18.17.0", "@types/react": "^18.2.45", "concurrently": "^8.2.2", diff --git a/templates/express/CHANGELOG.md b/templates/express/CHANGELOG.md index cb46d2130..35f49ffd0 100644 --- a/templates/express/CHANGELOG.md +++ b/templates/express/CHANGELOG.md @@ -1,5 +1,12 @@ # template-express +## 0.0.38 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.0.37 ### Patch Changes diff --git a/templates/express/package.json b/templates/express/package.json index 13c2a3a39..efad616f9 100644 --- a/templates/express/package.json +++ b/templates/express/package.json @@ -1,17 +1,17 @@ { "name": "template-express", - "version": "0.0.37", + "version": "0.0.38", "type": "module", "private": true, "dependencies": { "compression": "^1.7.4", "express": "^4.19.1", - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "react": "^18.2.0", "sirv": "^2.0.4" }, "devDependencies": { - "@frames.js/debugger": "^0.4.1", + "@frames.js/debugger": "^0.4.2", "@types/express": "^4.17.21", "@types/node": "^18.17.0", "@types/react": "^18.2.45", diff --git a/templates/hono/CHANGELOG.md b/templates/hono/CHANGELOG.md index 11481365b..dbb1eeff8 100644 --- a/templates/hono/CHANGELOG.md +++ b/templates/hono/CHANGELOG.md @@ -1,5 +1,12 @@ # template-hono +## 0.0.38 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.0.37 ### Patch Changes diff --git a/templates/hono/package.json b/templates/hono/package.json index adad6df9a..271f7ff11 100644 --- a/templates/hono/package.json +++ b/templates/hono/package.json @@ -1,16 +1,16 @@ { "name": "template-hono", - "version": "0.0.37", + "version": "0.0.38", "type": "module", "private": true, "dependencies": { "@hono/node-server": "^1.9.0", "hono": "^4.1.3", - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "react": "^18.2.0" }, "devDependencies": { - "@frames.js/debugger": "^0.4.1", + "@frames.js/debugger": "^0.4.2", "@hono/vite-dev-server": "^0.10.0", "@types/node": "^18.17.0", "@types/react": "^18.2.45", diff --git a/templates/next-starter-with-examples/CHANGELOG.md b/templates/next-starter-with-examples/CHANGELOG.md index 66436ed64..f03b10209 100644 --- a/templates/next-starter-with-examples/CHANGELOG.md +++ b/templates/next-starter-with-examples/CHANGELOG.md @@ -1,5 +1,12 @@ # template-next-starter-with-examples +## 0.1.14 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.1.13 ### Patch Changes diff --git a/templates/next-starter-with-examples/package.json b/templates/next-starter-with-examples/package.json index f8f3296d3..fbfb2820f 100644 --- a/templates/next-starter-with-examples/package.json +++ b/templates/next-starter-with-examples/package.json @@ -1,6 +1,6 @@ { "name": "template-next-starter-with-examples", - "version": "0.1.13", + "version": "0.1.14", "private": true, "type": "module", "scripts": { @@ -15,7 +15,7 @@ "@vercel/kv": "^1.0.1", "@xmtp/frames-validator": "^0.6.1", "clsx": "^2.1.0", - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "next": "^14.1.4", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -26,7 +26,7 @@ "node": ">=18.17.0" }, "devDependencies": { - "@frames.js/debugger": "^0.4.1", + "@frames.js/debugger": "^0.4.2", "@types/node": "^18.17.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", diff --git a/templates/next-utils-starter/CHANGELOG.md b/templates/next-utils-starter/CHANGELOG.md index 60f9d9f33..b17d9a421 100644 --- a/templates/next-utils-starter/CHANGELOG.md +++ b/templates/next-utils-starter/CHANGELOG.md @@ -1,5 +1,12 @@ # template-next-utils-starter +## 0.0.36 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.0.35 ### Patch Changes diff --git a/templates/next-utils-starter/package.json b/templates/next-utils-starter/package.json index b05d73e6f..209e1c4e4 100644 --- a/templates/next-utils-starter/package.json +++ b/templates/next-utils-starter/package.json @@ -1,6 +1,6 @@ { "name": "template-next-utils-starter", - "version": "0.0.35", + "version": "0.0.36", "private": true, "type": "module", "scripts": { @@ -11,14 +11,14 @@ "lint": "next lint" }, "dependencies": { - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "next": "^14.1.4", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@next/eslint-plugin-next": "^14.0.4", - "@frames.js/debugger": "^0.4.1", + "@frames.js/debugger": "^0.4.2", "@types/eslint": "^8.56.1", "@types/node": "^20.10.6", "@types/react": "^18.2.46", diff --git a/templates/next/CHANGELOG.md b/templates/next/CHANGELOG.md index 578f22bb4..b407c9dc3 100644 --- a/templates/next/CHANGELOG.md +++ b/templates/next/CHANGELOG.md @@ -1,5 +1,12 @@ # template-next +## 0.0.38 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.0.37 ### Patch Changes diff --git a/templates/next/package.json b/templates/next/package.json index d6310f877..85d3149a4 100644 --- a/templates/next/package.json +++ b/templates/next/package.json @@ -1,10 +1,10 @@ { "name": "template-next", - "version": "0.0.37", + "version": "0.0.38", "private": true, "type": "module", "dependencies": { - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "next": "^14.1.4", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -13,7 +13,7 @@ "@types/node": "^18.17.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", - "@frames.js/debugger": "^0.4.1", + "@frames.js/debugger": "^0.4.2", "concurrently": "^8.2.2", "dotenv": "^16.4.5", "is-port-reachable": "^4.0.0", diff --git a/templates/remix/CHANGELOG.md b/templates/remix/CHANGELOG.md index 27bdf80cd..ebe594eb9 100644 --- a/templates/remix/CHANGELOG.md +++ b/templates/remix/CHANGELOG.md @@ -1,5 +1,12 @@ # template-remix +## 0.0.38 + +### Patch Changes + +- Updated dependencies [86e03b9] + - frames.js@0.21.2 + ## 0.0.37 ### Patch Changes diff --git a/templates/remix/package.json b/templates/remix/package.json index ea3ca6534..1e58847cc 100644 --- a/templates/remix/package.json +++ b/templates/remix/package.json @@ -1,6 +1,6 @@ { "name": "template-remix", - "version": "0.0.37", + "version": "0.0.38", "private": true, "type": "module", "engines": { @@ -10,13 +10,13 @@ "@remix-run/node": "^2.8.1", "@remix-run/react": "^2.8.1", "@remix-run/serve": "^2.8.1", - "frames.js": "^0.21.1", + "frames.js": "^0.21.2", "isbot": "^4.1.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@frames.js/debugger": "^0.4.1", + "@frames.js/debugger": "^0.4.2", "@remix-run/dev": "^2.8.1", "@types/react": "^18.2.20", "@types/react-dom": "^18.2.7",