Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/api/phase-nft/custodian-release/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
return NextResponse.json({ ok: false, error: "Invalid recipientWallet." }, { status: 400 })
}

// Require recipient authorization: custodian release must be signed by recipient
const authHeader = request.headers.get("authorization") ?? ""
if (!authHeader.startsWith("Bearer ")) {
return NextResponse.json(
{ ok: false, code: "MISSING_AUTHORIZATION", error: "Authorization header required; recipient must sign release request." },
{ status: 401 },
)
}

const secret = process.env.CLASSIC_LIQ_ISSUER_SECRET?.trim()
if (!secret) {
return NextResponse.json(
Expand Down
41 changes: 35 additions & 6 deletions app/api/profile/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export async function GET(request: NextRequest) {
const q = request.nextUrl.searchParams.get("q")?.trim().toLowerCase() ?? ""
const filter = request.nextUrl.searchParams.get("filter") ?? "all"
const viewer = request.nextUrl.searchParams.get("viewer")?.trim() ?? ""
const cursor = request.nextUrl.searchParams.get("cursor") ?? ""

const [profiles, narratives, worldCollections, listingsFile, follows] = await Promise.all([
readJson<ProfileStore>(serverDataJsonPath("profileSocials")),
Expand Down Expand Up @@ -75,10 +76,16 @@ export async function GET(request: NextRequest) {
// Total collector count
const totalCollectors = Object.keys(profiles).length

// Build results from profiles
// Build results from profiles with cursor-based pagination
const allResults: SearchResult[] = []
const sortedWallets = Object.keys(profiles).sort()
const cursorIndex = cursor ? Math.max(0, sortedWallets.indexOf(cursor)) : 0
const pageSize = 10

for (let i = cursorIndex; i < sortedWallets.length && allResults.length < pageSize; i++) {
const wallet = sortedWallets[i]!
const data = profiles[wallet]!

for (const [wallet, data] of Object.entries(profiles)) {
const name = (data.display_name ?? "").toLowerCase()
const twitter = (data.twitter ?? "").toLowerCase()
const discord = (data.discord ?? "").toLowerCase()
Expand Down Expand Up @@ -120,13 +127,35 @@ export async function GET(request: NextRequest) {
allResults.sort((a, b) => b.artifact_count - a.artifact_count)

const results = allResults.slice(0, 10)
const nextCursor = results.length > 0 ? results[results.length - 1]!.wallet : null

// Suggested: top 5 by artifact_count when no query
const suggested: SearchResult[] = []
if (q.length < 2 && filter === "all") {
const top = [...allResults].sort((a, b) => b.artifact_count - a.artifact_count).slice(0, 5)
suggested.push(...top)
if (q.length < 2 && filter === "all" && !cursor) {
const sortedByArtifacts = [...sortedWallets]
.map((w) => ({
wallet: w,
artifactCount: walletArtifactCount.get(w) ?? 0,
}))
.sort((a, b) => b.artifactCount - a.artifactCount)
.slice(0, 5)

for (const { wallet } of sortedByArtifacts) {
const data = profiles[wallet]!
const worldName = walletWorldName.get(wallet) ?? null
suggested.push({
wallet,
display_name: data.display_name ?? null,
twitter: data.twitter ?? null,
discord: data.discord ?? null,
telegram: data.telegram ?? null,
artifact_count: walletArtifactCount.get(wallet) ?? 0,
has_world: worldName !== null,
world_name: worldName,
is_following: viewerFollowing.has(wallet),
})
}
}

return NextResponse.json({ results, suggested, totalCollectors })
return NextResponse.json({ results, suggested, totalCollectors, nextCursor })
}
44 changes: 44 additions & 0 deletions lib/notification-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,50 @@ export async function createNotification(
await writeStore(store)
}

export async function createNotificationBatch(
wallets: string[],
type: NotificationType,
data: Record<string, unknown>,
): Promise<{ succeeded: number; failed: number }> {
if (!wallets.length) return { succeeded: 0, failed: 0 }

const store = await readStore()
let succeeded = 0
let failed = 0
const now = Date.now()
const notificationId = randomUUID()

for (const wallet of wallets) {
try {
if (!(await shouldStoreNotification(wallet, type))) {
failed++
continue
}

const list = store[wallet] ?? []
const notif: Notification = {
id: notificationId,
wallet,
type,
read: false,
created_at: now,
data,
}
store[wallet] = [notif, ...list].slice(0, MAX_PER_WALLET)
succeeded++
} catch {
failed++
}
}

// Batch write
if (succeeded > 0) {
await writeStore(store)
}

return { succeeded, failed }
}

export async function getNotifications(wallet: string, limit = 30): Promise<Notification[]> {
const store = await readStore()
return (store[wallet] ?? []).slice(0, limit)
Expand Down