Merge branch 'main' into shreyas-formatting

This commit is contained in:
Shreyaschorge
2025-07-14 18:55:06 +05:30
34 changed files with 2479 additions and 829 deletions

View File

@@ -0,0 +1,16 @@
import { NextResponse } from 'next/server';
import { getNeynarClient } from '~/lib/neynar';
export async function GET() {
try {
const client = getNeynarClient();
const response = await client.fetchNonce();
return NextResponse.json(response);
} catch (error) {
console.error('Error fetching nonce:', error);
return NextResponse.json(
{ error: 'Failed to fetch nonce' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,43 @@
import { NextResponse } from 'next/server';
import { getNeynarClient } from '~/lib/neynar';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const message = searchParams.get('message');
const signature = searchParams.get('signature');
if (!message || !signature) {
return NextResponse.json(
{ error: 'Message and signature are required' },
{ status: 400 }
);
}
const client = getNeynarClient();
const data = await client.fetchSigners({ message, signature });
const signers = data.signers;
// Fetch user data if signers exist
let user = null;
if (signers && signers.length > 0 && signers[0].fid) {
const {
users: [fetchedUser],
} = await client.fetchBulkUsers({
fids: [signers[0].fid],
});
user = fetchedUser;
}
return NextResponse.json({
signers,
user,
});
} catch (error) {
console.error('Error in session-signers API:', error);
return NextResponse.json(
{ error: 'Failed to fetch signers' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,42 @@
import { NextResponse } from 'next/server';
import { getNeynarClient } from '~/lib/neynar';
export async function POST() {
try {
const neynarClient = getNeynarClient();
const signer = await neynarClient.createSigner();
return NextResponse.json(signer);
} catch (error) {
console.error('Error fetching signer:', error);
return NextResponse.json(
{ error: 'Failed to fetch signer' },
{ status: 500 }
);
}
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const signerUuid = searchParams.get('signerUuid');
if (!signerUuid) {
return NextResponse.json(
{ error: 'signerUuid is required' },
{ status: 400 }
);
}
try {
const neynarClient = getNeynarClient();
const signer = await neynarClient.lookupSigner({
signerUuid,
});
return NextResponse.json(signer);
} catch (error) {
console.error('Error fetching signed key:', error);
return NextResponse.json(
{ error: 'Failed to fetch signed key' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,91 @@
import { NextResponse } from 'next/server';
import { getNeynarClient } from '~/lib/neynar';
import { mnemonicToAccount } from 'viem/accounts';
import {
SIGNED_KEY_REQUEST_TYPE,
SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
} from '~/lib/constants';
const postRequiredFields = ['signerUuid', 'publicKey'];
export async function POST(request: Request) {
const body = await request.json();
// Validate required fields
for (const field of postRequiredFields) {
if (!body[field]) {
return NextResponse.json(
{ error: `${field} is required` },
{ status: 400 }
);
}
}
const { signerUuid, publicKey, redirectUrl } = body;
if (redirectUrl && typeof redirectUrl !== 'string') {
return NextResponse.json(
{ error: 'redirectUrl must be a string' },
{ status: 400 }
);
}
try {
// Get the app's account from seed phrase
const seedPhrase = process.env.SEED_PHRASE;
const shouldSponsor = process.env.SPONSOR_SIGNER === 'true';
if (!seedPhrase) {
return NextResponse.json(
{ error: 'App configuration missing (SEED_PHRASE or FID)' },
{ status: 500 }
);
}
const neynarClient = getNeynarClient();
const account = mnemonicToAccount(seedPhrase);
const {
user: { fid },
} = await neynarClient.lookupUserByCustodyAddress({
custodyAddress: account.address,
});
const appFid = fid;
// Generate deadline (24 hours from now)
const deadline = Math.floor(Date.now() / 1000) + 86400;
// Generate EIP-712 signature
const signature = await account.signTypedData({
domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
types: {
SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE,
},
primaryType: 'SignedKeyRequest',
message: {
requestFid: BigInt(appFid),
key: publicKey,
deadline: BigInt(deadline),
},
});
const signer = await neynarClient.registerSignedKey({
appFid,
deadline,
signature,
signerUuid,
...(redirectUrl && { redirectUrl }),
...(shouldSponsor && { sponsor: { sponsored_by_neynar: true } }),
});
return NextResponse.json(signer);
} catch (error) {
console.error('Error registering signed key:', error);
return NextResponse.json(
{ error: 'Failed to register signed key' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,38 @@
import { NextResponse } from 'next/server';
import { getNeynarClient } from '~/lib/neynar';
const requiredParams = ['message', 'signature'];
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const params: Record<string, string | null> = {};
for (const param of requiredParams) {
params[param] = searchParams.get(param);
if (!params[param]) {
return NextResponse.json(
{
error: `${param} parameter is required`,
},
{ status: 400 }
);
}
}
const message = params.message as string;
const signature = params.signature as string;
try {
const client = getNeynarClient();
const data = await client.fetchSigners({ message, signature });
const signers = data.signers;
return NextResponse.json({
signers,
});
} catch (error) {
console.error('Error fetching signers:', error);
return NextResponse.json(
{ error: 'Failed to fetch signers' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,46 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '~/auth';
export async function POST(request: Request) {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.fid) {
return NextResponse.json(
{ error: 'No authenticated session found' },
{ status: 401 }
);
}
const body = await request.json();
const { signers, user } = body;
if (!signers || !user) {
return NextResponse.json(
{ error: 'Signers and user are required' },
{ status: 400 }
);
}
// For NextAuth to update the session, we need to trigger the JWT callback
// This is typically done by calling the session endpoint with updated data
// However, we can't directly modify the session token from here
// Instead, we'll store the data temporarily and let the client refresh the session
// The session will be updated when the JWT callback is triggered
return NextResponse.json({
success: true,
message: 'Session update prepared',
signers,
user,
});
} catch (error) {
console.error('Error preparing session update:', error);
return NextResponse.json(
{ error: 'Failed to prepare session update' },
{ status: 500 }
);
}
}

View File

@@ -1,9 +1,9 @@
import { NextRequest } from 'next/server';
import { notificationDetailsSchema } from '@farcaster/frame-sdk';
import { z } from 'zod';
import { setUserNotificationDetails } from '~/lib/kv';
import { sendNeynarMiniAppNotification } from '~/lib/neynar';
import { sendMiniAppNotification } from '~/lib/notifs';
import { notificationDetailsSchema } from "@farcaster/miniapp-sdk";
import { NextRequest } from "next/server";
import { z } from "zod";
import { setUserNotificationDetails } from "~/lib/kv";
import { sendMiniAppNotification } from "~/lib/notifs";
import { sendNeynarMiniAppNotification } from "~/lib/neynar";
const requestSchema = z.object({
fid: z.number(),
@@ -13,8 +13,7 @@ const requestSchema = z.object({
export async function POST(request: NextRequest) {
// If Neynar is enabled, we don't need to store notification details
// as they will be managed by Neynar's system
const neynarEnabled =
process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
const requestJson = await request.json();
const requestBody = requestSchema.safeParse(requestJson);
@@ -22,7 +21,7 @@ export async function POST(request: NextRequest) {
if (requestBody.success === false) {
return Response.json(
{ success: false, errors: requestBody.error.errors },
{ status: 400 },
{ status: 400 }
);
}
@@ -30,31 +29,29 @@ export async function POST(request: NextRequest) {
if (!neynarEnabled) {
await setUserNotificationDetails(
Number(requestBody.data.fid),
requestBody.data.notificationDetails,
requestBody.data.notificationDetails
);
}
// Use appropriate notification function based on Neynar status
const sendNotification = neynarEnabled
? sendNeynarMiniAppNotification
: sendMiniAppNotification;
const sendNotification = neynarEnabled ? sendNeynarMiniAppNotification : sendMiniAppNotification;
const sendResult = await sendNotification({
fid: Number(requestBody.data.fid),
title: 'Test notification',
body: 'Sent at ' + new Date().toISOString(),
title: "Test notification",
body: "Sent at " + new Date().toISOString(),
});
if (sendResult.state === 'error') {
if (sendResult.state === "error") {
return Response.json(
{ success: false, error: sendResult.error },
{ status: 500 },
{ status: 500 }
);
} else if (sendResult.state === 'rate_limit') {
} else if (sendResult.state === "rate_limit") {
return Response.json(
{ success: false, error: 'Rate limited' },
{ status: 429 },
{ success: false, error: "Rate limited" },
{ status: 429 }
);
}
return Response.json({ success: true });
}
}

View File

@@ -1,21 +1,20 @@
import { NextRequest } from 'next/server';
import {
ParseWebhookEvent,
parseWebhookEvent,
verifyAppKeyWithNeynar,
} from '@farcaster/frame-node';
import { APP_NAME } from '~/lib/constants';
} from "@farcaster/miniapp-node";
import { NextRequest } from "next/server";
import { APP_NAME } from "~/lib/constants";
import {
deleteUserNotificationDetails,
setUserNotificationDetails,
} from '~/lib/kv';
import { sendMiniAppNotification } from '~/lib/notifs';
} from "~/lib/kv";
import { sendMiniAppNotification } from "~/lib/notifs";
export async function POST(request: NextRequest) {
// If Neynar is enabled, we don't need to handle webhooks here
// as they will be handled by Neynar's webhook endpoint
const neynarEnabled =
process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
if (neynarEnabled) {
return Response.json({ success: true });
}
@@ -29,24 +28,24 @@ export async function POST(request: NextRequest) {
const error = e as ParseWebhookEvent.ErrorType;
switch (error.name) {
case 'VerifyJsonFarcasterSignature.InvalidDataError':
case 'VerifyJsonFarcasterSignature.InvalidEventDataError':
case "VerifyJsonFarcasterSignature.InvalidDataError":
case "VerifyJsonFarcasterSignature.InvalidEventDataError":
// The request data is invalid
return Response.json(
{ success: false, error: error.message },
{ status: 400 },
{ status: 400 }
);
case 'VerifyJsonFarcasterSignature.InvalidAppKeyError':
case "VerifyJsonFarcasterSignature.InvalidAppKeyError":
// The app key is invalid
return Response.json(
{ success: false, error: error.message },
{ status: 401 },
{ status: 401 }
);
case 'VerifyJsonFarcasterSignature.VerifyAppKeyError':
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 },
{ status: 500 }
);
}
}
@@ -57,36 +56,36 @@ export async function POST(request: NextRequest) {
// Only handle notifications if Neynar is not enabled
// When Neynar is enabled, notifications are handled through their webhook
switch (event.event) {
case 'frame_added':
case "frame_added":
if (event.notificationDetails) {
await setUserNotificationDetails(fid, event.notificationDetails);
await sendMiniAppNotification({
fid,
title: `Welcome to ${APP_NAME}`,
body: 'Mini app is now added to your client',
body: "Mini app is now added to your client",
});
} else {
await deleteUserNotificationDetails(fid);
}
break;
case 'frame_removed':
case "frame_removed":
await deleteUserNotificationDetails(fid);
break;
case 'notifications_enabled':
case "notifications_enabled":
await setUserNotificationDetails(fid, event.notificationDetails);
await sendMiniAppNotification({
fid,
title: `Welcome to ${APP_NAME}`,
body: 'Notifications are now enabled',
body: "Notifications are now enabled",
});
break;
case 'notifications_disabled':
case "notifications_disabled":
await deleteUserNotificationDetails(fid);
break;
}
return Response.json({ success: true });
}
}