@farcaster/frame-node 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # Frame Backend (Node) SDK
2
+
3
+ Provides utility methods for the backend of V2 frames:
4
+
5
+ - `parseWebhookEvent`: parses and verifies webhook events, using a `VerifyAppKey` method
6
+ - `verifyJsonFarcasterSignature`: verifies a [JSON Farcaster Signature](https://github.com/farcasterxyz/protocol/discussions/208) payload, using a `VerifyAppKey` method
7
+ - `createJsonFarcasterSignature`: creates a [JSON Farcaster Signature](https://github.com/farcasterxyz/protocol/discussions/208) payload
8
+
9
+ For signature verification, you need to pass in a `VerifyAppKey` method that verifies that an app key is valid for an FID. You can use the included `verifyAppKeyWithNeynar` which uses [Neynar](https://neynar.com) and requires the `NEYNAR_API_KEY` environment variable to be defined.
10
+
11
+ Not yet stable. [Learn more](https://github.com/farcasterxyz/frames/wiki/frames-v2-developer-playground-preview).
12
+
13
+ ## Install
14
+
15
+ Install using your favorite manager:
16
+
17
+ ```
18
+ npm install @farcaster/frame-node
19
+ ```
@@ -0,0 +1,6 @@
1
+ export * from "@farcaster/frame-core";
2
+ export * from "./jfs";
3
+ export * from "./neynar";
4
+ export * from "./types";
5
+ export * from "./webhook";
6
+ export * from "./util";
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("@farcaster/frame-core"), exports);
18
+ __exportStar(require("./jfs"), exports);
19
+ __exportStar(require("./neynar"), exports);
20
+ __exportStar(require("./types"), exports);
21
+ __exportStar(require("./webhook"), exports);
22
+ __exportStar(require("./util"), exports);
package/dist/jfs.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { EncodedJsonFarcasterSignatureSchema } from "@farcaster/frame-core";
2
+ import { BaseError, VerifyAppKey, VerifyJfsResult } from "./types";
3
+ export declare namespace VerifyJsonFarcasterSignature {
4
+ type ErrorType = InvalidJfsDataError | InvalidJfsAppKeyError | VerifyAppKeyError;
5
+ }
6
+ export declare class InvalidJfsDataError<C extends Error | undefined = undefined> extends BaseError<C> {
7
+ readonly name = "VerifyJsonFarcasterSignature.InvalidDataError";
8
+ }
9
+ export declare class InvalidJfsAppKeyError<C extends Error | undefined = undefined> extends BaseError<C> {
10
+ readonly name = "VerifyJsonFarcasterSignature.InvalidAppKeyError";
11
+ }
12
+ export declare class VerifyAppKeyError<C extends Error | undefined = undefined> extends BaseError<C> {
13
+ readonly name = "VerifyJsonFarcasterSignature.VerifyAppKeyError";
14
+ }
15
+ export declare function verifyJsonFarcasterSignature(data: unknown, verifyAppKey: VerifyAppKey): Promise<VerifyJfsResult>;
16
+ export declare function createJsonFarcasterSignature({ fid, type, privateKey, payload, }: {
17
+ fid: number;
18
+ type: "app_key";
19
+ privateKey: Uint8Array;
20
+ payload: Uint8Array;
21
+ }): EncodedJsonFarcasterSignatureSchema;
package/dist/jfs.js ADDED
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VerifyAppKeyError = exports.InvalidJfsAppKeyError = exports.InvalidJfsDataError = void 0;
4
+ exports.verifyJsonFarcasterSignature = verifyJsonFarcasterSignature;
5
+ exports.createJsonFarcasterSignature = createJsonFarcasterSignature;
6
+ const frame_core_1 = require("@farcaster/frame-core");
7
+ const ed25519_1 = require("@noble/curves/ed25519");
8
+ const types_1 = require("./types");
9
+ const util_1 = require("./util");
10
+ class InvalidJfsDataError extends types_1.BaseError {
11
+ constructor() {
12
+ super(...arguments);
13
+ this.name = "VerifyJsonFarcasterSignature.InvalidDataError";
14
+ }
15
+ }
16
+ exports.InvalidJfsDataError = InvalidJfsDataError;
17
+ class InvalidJfsAppKeyError extends types_1.BaseError {
18
+ constructor() {
19
+ super(...arguments);
20
+ this.name = "VerifyJsonFarcasterSignature.InvalidAppKeyError";
21
+ }
22
+ }
23
+ exports.InvalidJfsAppKeyError = InvalidJfsAppKeyError;
24
+ class VerifyAppKeyError extends types_1.BaseError {
25
+ constructor() {
26
+ super(...arguments);
27
+ this.name = "VerifyJsonFarcasterSignature.VerifyAppKeyError";
28
+ }
29
+ }
30
+ exports.VerifyAppKeyError = VerifyAppKeyError;
31
+ async function verifyJsonFarcasterSignature(data, verifyAppKey) {
32
+ //
33
+ // Parse, decode and validate data
34
+ //
35
+ const body = frame_core_1.encodedJsonFarcasterSignatureSchema.safeParse(data);
36
+ if (body.success === false) {
37
+ throw new InvalidJfsDataError("Error parsing data", body.error);
38
+ }
39
+ let headerData;
40
+ try {
41
+ headerData = JSON.parse(Buffer.from(body.data.header, "base64url").toString("utf-8"));
42
+ }
43
+ catch (error) {
44
+ throw new InvalidJfsDataError("Error decoding and parsing header");
45
+ }
46
+ const header = frame_core_1.jsonFarcasterSignatureHeaderSchema.safeParse(headerData);
47
+ if (header.success === false) {
48
+ throw new InvalidJfsDataError("Error parsing header", header.error);
49
+ }
50
+ const payload = Buffer.from(body.data.payload, "base64url");
51
+ const signature = Buffer.from(body.data.signature, "base64url");
52
+ if (signature.byteLength !== 64) {
53
+ throw new InvalidJfsDataError("Invalid signature length");
54
+ }
55
+ //
56
+ // Verify the signature
57
+ //
58
+ const fid = header.data.fid;
59
+ const appKey = header.data.key;
60
+ const appKeyBytes = (0, util_1.hexToBytes)(appKey);
61
+ const signedInput = new Uint8Array(Buffer.from(body.data.header + "." + body.data.payload));
62
+ let verifyResult;
63
+ try {
64
+ verifyResult = ed25519_1.ed25519.verify(signature, signedInput, appKeyBytes);
65
+ }
66
+ catch (e) {
67
+ throw new InvalidJfsDataError("Error checking signature", e instanceof Error ? e : undefined);
68
+ }
69
+ if (!verifyResult) {
70
+ throw new InvalidJfsDataError("Invalid signature");
71
+ }
72
+ //
73
+ // Verify that the app key belongs to the FID
74
+ //
75
+ let appKeyResult;
76
+ try {
77
+ appKeyResult = await verifyAppKey(fid, appKey);
78
+ }
79
+ catch (error) {
80
+ throw new VerifyAppKeyError("Error verifying app key", error instanceof Error ? error : undefined);
81
+ }
82
+ if (!appKeyResult.valid) {
83
+ throw new InvalidJfsAppKeyError("App key not valid for FID");
84
+ }
85
+ return { fid, appFid: appKeyResult.appFid, payload };
86
+ }
87
+ function createJsonFarcasterSignature({ fid, type, privateKey, payload, }) {
88
+ const publicKey = ed25519_1.ed25519.getPublicKey(privateKey);
89
+ const header = { fid, type, key: (0, util_1.bytesToHex)(publicKey) };
90
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url");
91
+ const encodedPayload = Buffer.from(payload).toString("base64url");
92
+ const signatureInput = new Uint8Array(Buffer.from(encodedHeader + "." + encodedPayload, "utf-8"));
93
+ const signature = ed25519_1.ed25519.sign(signatureInput, privateKey);
94
+ const encodedSignature = Buffer.from(signature).toString("base64url");
95
+ return {
96
+ header: encodedHeader,
97
+ payload: encodedPayload,
98
+ signature: encodedSignature,
99
+ };
100
+ }
@@ -0,0 +1,19 @@
1
+ import { VerifyAppKey } from "./types";
2
+ export declare const signedKeyRequestAbi: readonly [{
3
+ readonly components: readonly [{
4
+ readonly name: "requestFid";
5
+ readonly type: "uint256";
6
+ }, {
7
+ readonly name: "requestSigner";
8
+ readonly type: "address";
9
+ }, {
10
+ readonly name: "signature";
11
+ readonly type: "bytes";
12
+ }, {
13
+ readonly name: "deadline";
14
+ readonly type: "uint256";
15
+ }];
16
+ readonly name: "SignedKeyRequest";
17
+ readonly type: "tuple";
18
+ }];
19
+ export declare const verifyAppKeyWithNeynar: VerifyAppKey;
package/dist/neynar.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyAppKeyWithNeynar = exports.signedKeyRequestAbi = void 0;
4
+ const types_1 = require("./types");
5
+ const zod_1 = require("zod");
6
+ const ox_1 = require("ox");
7
+ const apiKey = process.env.NEYNAR_API_KEY || "";
8
+ exports.signedKeyRequestAbi = [
9
+ {
10
+ components: [
11
+ {
12
+ name: "requestFid",
13
+ type: "uint256",
14
+ },
15
+ {
16
+ name: "requestSigner",
17
+ type: "address",
18
+ },
19
+ {
20
+ name: "signature",
21
+ type: "bytes",
22
+ },
23
+ {
24
+ name: "deadline",
25
+ type: "uint256",
26
+ },
27
+ ],
28
+ name: "SignedKeyRequest",
29
+ type: "tuple",
30
+ },
31
+ ];
32
+ const neynarResponseSchema = zod_1.z.object({
33
+ events: zod_1.z.array(zod_1.z.object({
34
+ signerEventBody: zod_1.z.object({
35
+ key: zod_1.z.string(),
36
+ metadata: zod_1.z.string(),
37
+ }),
38
+ })),
39
+ });
40
+ const verifyAppKeyWithNeynar = async (fid, appKey) => {
41
+ if (!apiKey) {
42
+ throw new Error("Environment variable NEYNAR_API_KEY needs to be set to use Neynar for app key verification");
43
+ }
44
+ const url = new URL("https://hub-api.neynar.com/v1/onChainSignersByFid");
45
+ url.searchParams.append("fid", fid.toString());
46
+ const response = await fetch(url, {
47
+ method: "GET",
48
+ headers: {
49
+ "x-api-key": apiKey,
50
+ },
51
+ });
52
+ if (response.status !== 200) {
53
+ throw new types_1.BaseError(`Non-200 response received: ${await response.text()}`);
54
+ }
55
+ const responseJson = await response.json();
56
+ const parsedResponse = neynarResponseSchema.safeParse(responseJson);
57
+ if (parsedResponse.error) {
58
+ throw new types_1.BaseError("Error parsing Neynar response", parsedResponse.error);
59
+ }
60
+ const appKeyLower = appKey.toLowerCase();
61
+ const signerEvent = parsedResponse.data.events.find((event) => event.signerEventBody.key.toLowerCase() === appKeyLower);
62
+ if (!signerEvent) {
63
+ return { valid: false };
64
+ }
65
+ const decoded = ox_1.AbiParameters.decode(exports.signedKeyRequestAbi, Buffer.from(signerEvent.signerEventBody.metadata, "base64"));
66
+ if (decoded.length !== 1) {
67
+ throw new types_1.BaseError("Error decoding metadata");
68
+ }
69
+ const appFid = Number(decoded[0].requestFid);
70
+ return { valid: true, appFid };
71
+ };
72
+ exports.verifyAppKeyWithNeynar = verifyAppKeyWithNeynar;
@@ -0,0 +1,23 @@
1
+ import { FrameEvent } from "@farcaster/frame-core";
2
+ export type VerifyAppKeyResult = {
3
+ valid: true;
4
+ appFid: number;
5
+ } | {
6
+ valid: false;
7
+ };
8
+ export type VerifyAppKey = (fid: number, appKey: string) => Promise<VerifyAppKeyResult>;
9
+ export type VerifyJfsResult = {
10
+ fid: number;
11
+ appFid: number;
12
+ payload: Uint8Array;
13
+ };
14
+ export type ParseWebhookEventResult = {
15
+ fid: number;
16
+ appFid: number;
17
+ event: FrameEvent;
18
+ };
19
+ export declare class BaseError<C extends Error | undefined = undefined> extends Error {
20
+ name: string;
21
+ cause: C;
22
+ constructor(message: string, cause?: C);
23
+ }
package/dist/types.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseError = void 0;
4
+ class BaseError extends Error {
5
+ constructor(message, cause) {
6
+ super(message);
7
+ this.name = "BaseError";
8
+ this.cause = cause;
9
+ }
10
+ }
11
+ exports.BaseError = BaseError;
package/dist/util.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function bytesToHex(bytes: Uint8Array): string;
2
+ export declare function hexToBytes(hex: string): Uint8Array;
package/dist/util.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bytesToHex = bytesToHex;
4
+ exports.hexToBytes = hexToBytes;
5
+ function bytesToHex(bytes) {
6
+ return `0x${Buffer.from(bytes).toString("hex")}`;
7
+ }
8
+ function hexToBytes(hex) {
9
+ return Uint8Array.from(Buffer.from(hex.startsWith("0x") ? hex.slice(2) : hex, "hex"));
10
+ }
@@ -0,0 +1,9 @@
1
+ import { VerifyJsonFarcasterSignature } from "./jfs";
2
+ import { BaseError, ParseWebhookEventResult, VerifyAppKey } from "./types";
3
+ export declare namespace ParseWebhookEvent {
4
+ type ErrorType = VerifyJsonFarcasterSignature.ErrorType | InvalidEventDataError;
5
+ }
6
+ export declare class InvalidEventDataError<C extends Error | undefined = undefined> extends BaseError<C> {
7
+ readonly name = "VerifyJsonFarcasterSignature.InvalidEventDataError";
8
+ }
9
+ export declare function parseWebhookEvent(rawData: unknown, verifyAppKey: VerifyAppKey): Promise<ParseWebhookEventResult>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvalidEventDataError = void 0;
4
+ exports.parseWebhookEvent = parseWebhookEvent;
5
+ const frame_core_1 = require("@farcaster/frame-core");
6
+ const jfs_1 = require("./jfs");
7
+ const types_1 = require("./types");
8
+ class InvalidEventDataError extends types_1.BaseError {
9
+ constructor() {
10
+ super(...arguments);
11
+ this.name = "VerifyJsonFarcasterSignature.InvalidEventDataError";
12
+ }
13
+ }
14
+ exports.InvalidEventDataError = InvalidEventDataError;
15
+ async function parseWebhookEvent(rawData, verifyAppKey) {
16
+ const { fid, appFid, payload } = await (0, jfs_1.verifyJsonFarcasterSignature)(rawData, verifyAppKey);
17
+ // Pase and validate event payload
18
+ let payloadJson;
19
+ try {
20
+ payloadJson = JSON.parse(Buffer.from(payload).toString("utf-8"));
21
+ }
22
+ catch (error) {
23
+ throw new InvalidEventDataError("Error decoding and parsing payload", error instanceof Error ? error : undefined);
24
+ }
25
+ const event = frame_core_1.eventPayloadSchema.safeParse(payload);
26
+ if (event.success === false) {
27
+ throw new InvalidEventDataError("Invalid event payload", event.error);
28
+ }
29
+ return { fid, appFid, event: event.data };
30
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@farcaster/frame-node",
3
+ "version": "0.0.2",
4
+ "main": "dist/index.js",
5
+ "module": "esm/index.js",
6
+ "scripts": {
7
+ "clean": "rm -rf dist esm",
8
+ "prebuild": "npm run clean",
9
+ "build": "yarn build:cjs & yarn build:esm",
10
+ "build:cjs": "tsc -p tsconfig.node.json",
11
+ "build:esm": "tsc -p tsconfig.json",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest --coverage"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "devDependencies": {
22
+ "@farcaster/tsconfig": "*",
23
+ "@vitest/coverage-v8": "^2.1.8",
24
+ "tsup": "^8.3.5",
25
+ "typescript": "^5.6.3",
26
+ "vitest": "^2.1.8"
27
+ },
28
+ "dependencies": {
29
+ "@farcaster/frame-core": "^0.0.15",
30
+ "ox": "^0.4.0"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from "@farcaster/frame-core";
2
+ export * from "./jfs";
3
+ export * from "./neynar";
4
+ export * from "./types";
5
+ export * from "./webhook";
6
+ export * from "./util";
package/src/jfs.ts ADDED
@@ -0,0 +1,146 @@
1
+ import {
2
+ EncodedJsonFarcasterSignatureSchema,
3
+ encodedJsonFarcasterSignatureSchema,
4
+ jsonFarcasterSignatureHeaderSchema,
5
+ } from "@farcaster/frame-core";
6
+ import { ed25519 } from "@noble/curves/ed25519";
7
+ import { BaseError, VerifyAppKey, VerifyJfsResult } from "./types";
8
+ import { bytesToHex, hexToBytes } from "./util";
9
+
10
+ export declare namespace VerifyJsonFarcasterSignature {
11
+ type ErrorType =
12
+ | InvalidJfsDataError
13
+ | InvalidJfsAppKeyError
14
+ | VerifyAppKeyError;
15
+ }
16
+
17
+ export class InvalidJfsDataError<
18
+ C extends Error | undefined = undefined,
19
+ > extends BaseError<C> {
20
+ override readonly name = "VerifyJsonFarcasterSignature.InvalidDataError";
21
+ }
22
+
23
+ export class InvalidJfsAppKeyError<
24
+ C extends Error | undefined = undefined,
25
+ > extends BaseError<C> {
26
+ override readonly name = "VerifyJsonFarcasterSignature.InvalidAppKeyError";
27
+ }
28
+
29
+ export class VerifyAppKeyError<
30
+ C extends Error | undefined = undefined,
31
+ > extends BaseError<C> {
32
+ override readonly name = "VerifyJsonFarcasterSignature.VerifyAppKeyError";
33
+ }
34
+
35
+ export async function verifyJsonFarcasterSignature(
36
+ data: unknown,
37
+ verifyAppKey: VerifyAppKey,
38
+ ): Promise<VerifyJfsResult> {
39
+ //
40
+ // Parse, decode and validate data
41
+ //
42
+
43
+ const body = encodedJsonFarcasterSignatureSchema.safeParse(data);
44
+ if (body.success === false) {
45
+ throw new InvalidJfsDataError("Error parsing data", body.error);
46
+ }
47
+
48
+ let headerData;
49
+ try {
50
+ headerData = JSON.parse(
51
+ Buffer.from(body.data.header, "base64url").toString("utf-8"),
52
+ );
53
+ } catch (error: unknown) {
54
+ throw new InvalidJfsDataError("Error decoding and parsing header");
55
+ }
56
+
57
+ const header = jsonFarcasterSignatureHeaderSchema.safeParse(headerData);
58
+ if (header.success === false) {
59
+ throw new InvalidJfsDataError("Error parsing header", header.error);
60
+ }
61
+
62
+ const payload = Buffer.from(body.data.payload, "base64url");
63
+
64
+ const signature = Buffer.from(body.data.signature, "base64url");
65
+ if (signature.byteLength !== 64) {
66
+ throw new InvalidJfsDataError("Invalid signature length");
67
+ }
68
+
69
+ //
70
+ // Verify the signature
71
+ //
72
+
73
+ const fid = header.data.fid;
74
+ const appKey = header.data.key;
75
+ const appKeyBytes = hexToBytes(appKey);
76
+
77
+ const signedInput = new Uint8Array(
78
+ Buffer.from(body.data.header + "." + body.data.payload),
79
+ );
80
+
81
+ let verifyResult;
82
+ try {
83
+ verifyResult = ed25519.verify(signature, signedInput, appKeyBytes);
84
+ } catch (e) {
85
+ throw new InvalidJfsDataError(
86
+ "Error checking signature",
87
+ e instanceof Error ? e : undefined,
88
+ );
89
+ }
90
+
91
+ if (!verifyResult) {
92
+ throw new InvalidJfsDataError("Invalid signature");
93
+ }
94
+
95
+ //
96
+ // Verify that the app key belongs to the FID
97
+ //
98
+
99
+ let appKeyResult;
100
+ try {
101
+ appKeyResult = await verifyAppKey(fid, appKey);
102
+ } catch (error: unknown) {
103
+ throw new VerifyAppKeyError(
104
+ "Error verifying app key",
105
+ error instanceof Error ? error : undefined,
106
+ );
107
+ }
108
+
109
+ if (!appKeyResult.valid) {
110
+ throw new InvalidJfsAppKeyError("App key not valid for FID");
111
+ }
112
+
113
+ return { fid, appFid: appKeyResult.appFid, payload };
114
+ }
115
+
116
+ export function createJsonFarcasterSignature({
117
+ fid,
118
+ type,
119
+ privateKey,
120
+ payload,
121
+ }: {
122
+ fid: number;
123
+ type: "app_key";
124
+ privateKey: Uint8Array;
125
+ payload: Uint8Array;
126
+ }): EncodedJsonFarcasterSignatureSchema {
127
+ const publicKey = ed25519.getPublicKey(privateKey);
128
+
129
+ const header = { fid, type, key: bytesToHex(publicKey) };
130
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString(
131
+ "base64url",
132
+ );
133
+ const encodedPayload = Buffer.from(payload).toString("base64url");
134
+ const signatureInput = new Uint8Array(
135
+ Buffer.from(encodedHeader + "." + encodedPayload, "utf-8"),
136
+ );
137
+
138
+ const signature = ed25519.sign(signatureInput, privateKey);
139
+ const encodedSignature = Buffer.from(signature).toString("base64url");
140
+
141
+ return {
142
+ header: encodedHeader,
143
+ payload: encodedPayload,
144
+ signature: encodedSignature,
145
+ };
146
+ }
package/src/neynar.ts ADDED
@@ -0,0 +1,93 @@
1
+ import { BaseError, VerifyAppKey, VerifyAppKeyResult } from "./types";
2
+ import { z } from "zod";
3
+ import { AbiParameters } from "ox";
4
+
5
+ const apiKey = process.env.NEYNAR_API_KEY || "";
6
+
7
+ export const signedKeyRequestAbi = [
8
+ {
9
+ components: [
10
+ {
11
+ name: "requestFid",
12
+ type: "uint256",
13
+ },
14
+ {
15
+ name: "requestSigner",
16
+ type: "address",
17
+ },
18
+ {
19
+ name: "signature",
20
+ type: "bytes",
21
+ },
22
+ {
23
+ name: "deadline",
24
+ type: "uint256",
25
+ },
26
+ ],
27
+ name: "SignedKeyRequest",
28
+ type: "tuple",
29
+ },
30
+ ] as const;
31
+
32
+ const neynarResponseSchema = z.object({
33
+ events: z.array(
34
+ z.object({
35
+ signerEventBody: z.object({
36
+ key: z.string(),
37
+ metadata: z.string(),
38
+ }),
39
+ }),
40
+ ),
41
+ });
42
+
43
+ export const verifyAppKeyWithNeynar: VerifyAppKey = async (
44
+ fid: number,
45
+ appKey: string,
46
+ ): Promise<VerifyAppKeyResult> => {
47
+ if (!apiKey) {
48
+ throw new Error(
49
+ "Environment variable NEYNAR_API_KEY needs to be set to use Neynar for app key verification",
50
+ );
51
+ }
52
+
53
+ const url = new URL("https://hub-api.neynar.com/v1/onChainSignersByFid");
54
+ url.searchParams.append("fid", fid.toString());
55
+
56
+ const response = await fetch(url, {
57
+ method: "GET",
58
+ headers: {
59
+ "x-api-key": apiKey,
60
+ },
61
+ });
62
+
63
+ if (response.status !== 200) {
64
+ throw new BaseError(`Non-200 response received: ${await response.text()}`);
65
+ }
66
+
67
+ const responseJson = await response.json();
68
+ const parsedResponse = neynarResponseSchema.safeParse(responseJson);
69
+ if (parsedResponse.error) {
70
+ throw new BaseError("Error parsing Neynar response", parsedResponse.error);
71
+ }
72
+
73
+ const appKeyLower = appKey.toLowerCase();
74
+
75
+ const signerEvent = parsedResponse.data.events.find(
76
+ (event) => event.signerEventBody.key.toLowerCase() === appKeyLower,
77
+ );
78
+ if (!signerEvent) {
79
+ return { valid: false };
80
+ }
81
+
82
+ const decoded = AbiParameters.decode(
83
+ signedKeyRequestAbi,
84
+ Buffer.from(signerEvent.signerEventBody.metadata, "base64"),
85
+ );
86
+ if (decoded.length !== 1) {
87
+ throw new BaseError("Error decoding metadata");
88
+ }
89
+
90
+ const appFid = Number(decoded[0].requestFid);
91
+
92
+ return { valid: true, appFid };
93
+ };
package/src/types.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { FrameEvent } from "@farcaster/frame-core";
2
+
3
+ export type VerifyAppKeyResult =
4
+ | { valid: true; appFid: number }
5
+ | { valid: false };
6
+
7
+ export type VerifyAppKey = (
8
+ fid: number,
9
+ appKey: string,
10
+ ) => Promise<VerifyAppKeyResult>;
11
+
12
+ export type VerifyJfsResult = {
13
+ fid: number;
14
+ appFid: number;
15
+ payload: Uint8Array;
16
+ };
17
+
18
+ export type ParseWebhookEventResult = {
19
+ fid: number;
20
+ appFid: number;
21
+ event: FrameEvent;
22
+ };
23
+
24
+ export class BaseError<C extends Error | undefined = undefined> extends Error {
25
+ override name = "BaseError";
26
+ cause: C;
27
+
28
+ constructor(message: string, cause?: C) {
29
+ super(message);
30
+ this.cause = cause as any;
31
+ }
32
+ }
package/src/util.ts ADDED
@@ -0,0 +1,9 @@
1
+ export function bytesToHex(bytes: Uint8Array): string {
2
+ return `0x${Buffer.from(bytes).toString("hex")}`;
3
+ }
4
+
5
+ export function hexToBytes(hex: string): Uint8Array {
6
+ return Uint8Array.from(
7
+ Buffer.from(hex.startsWith("0x") ? hex.slice(2) : hex, "hex"),
8
+ );
9
+ }
package/src/webhook.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { eventPayloadSchema } from "@farcaster/frame-core";
2
+ import {
3
+ VerifyJsonFarcasterSignature,
4
+ verifyJsonFarcasterSignature,
5
+ } from "./jfs";
6
+ import { BaseError, ParseWebhookEventResult, VerifyAppKey } from "./types";
7
+
8
+ export declare namespace ParseWebhookEvent {
9
+ type ErrorType =
10
+ | VerifyJsonFarcasterSignature.ErrorType
11
+ | InvalidEventDataError;
12
+ }
13
+
14
+ export class InvalidEventDataError<
15
+ C extends Error | undefined = undefined,
16
+ > extends BaseError<C> {
17
+ override readonly name = "VerifyJsonFarcasterSignature.InvalidEventDataError";
18
+ }
19
+
20
+ export async function parseWebhookEvent(
21
+ rawData: unknown,
22
+ verifyAppKey: VerifyAppKey,
23
+ ): Promise<ParseWebhookEventResult> {
24
+ const { fid, appFid, payload } = await verifyJsonFarcasterSignature(
25
+ rawData,
26
+ verifyAppKey,
27
+ );
28
+
29
+ // Pase and validate event payload
30
+ let payloadJson;
31
+ try {
32
+ payloadJson = JSON.parse(Buffer.from(payload).toString("utf-8"));
33
+ } catch (error: unknown) {
34
+ throw new InvalidEventDataError(
35
+ "Error decoding and parsing payload",
36
+ error instanceof Error ? error : undefined,
37
+ );
38
+ }
39
+
40
+ const event = eventPayloadSchema.safeParse(payload);
41
+ if (event.success === false) {
42
+ throw new InvalidEventDataError("Invalid event payload", event.error);
43
+ }
44
+
45
+ return { fid, appFid, event: event.data };
46
+ }