/**
* Copyright (C) 2026 SofĂa Aritz and contributors
*
* This file is part of Everest.
*
* Everest is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* Everest is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Everest. If not, see .
*/
import { deleteAccount, deleteEntry, deleteFollow, upsertAccount, upsertEntry, upsertFollow } from "@/lib/db/queries";
import { AtUri } from "@atproto/syntax";
import { assureAdminAuth, parseTapEvent } from "@atproto/tap";
import { NextRequest, NextResponse } from "next/server";
import * as com from "@/lexicons/com";
import * as app from "@/lexicons/app";
import { Client } from "@atproto/lex";
const TAP_ADMIN_PASSWORD = process.env.TAP_ADMIN_PASSWORD;
function tryParseRecord(record: R, parseFunc: (record: R) => T): T | null {
try {
return parseFunc(record);
} catch {
return null;
}
}
export async function POST(request: NextRequest) {
if (TAP_ADMIN_PASSWORD) {
const authHeader = request.headers.get("Authorization");
if (!authHeader) {
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 });
}
try {
assureAdminAuth(TAP_ADMIN_PASSWORD, authHeader);
} catch {
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 });
}
}
const body = await request.json();
const event = parseTapEvent(body);
if (event.type === "identity") {
console.log("[tap/webhook] event received (type=identity)");
if (event.status === "deleted") {
console.log("[tap/webhook] |> event.status=deleted for did", event.did);
await deleteAccount(event.did);
} else {
// FIXME(sofiaritz.com): Allow customization through env vars
const client = new Client("https://public.api.bsky.app");
const profile = await client.call(app.bsky.actor.getProfile, {
actor: event.did,
});
const updatedAccount = {
did: event.did,
handle: event.handle,
active: event.isActive,
avatar: profile.avatar,
};
console.log("[tap/webhook] |> event.status!=deleted for did", event.did);
console.log("[tap/webhook] |> new account data", updatedAccount);
await upsertAccount(updatedAccount);
}
}
if (event.type === "record") {
console.log("[tap/webhook] event received (type=record)");
const uri = AtUri.make(event.did, event.collection, event.rkey);
if (event.action === "create" || event.action === "update") {
console.log("[tap/webhook] |> event.action=create|update for record", uri.toString());
if (event.collection === com.sofiaritz.everest.entry.$type) {
const record = tryParseRecord(
event.record, com.sofiaritz.everest.entry.$parse,
);
if (!record) return NextResponse.json({ success: false });
await upsertEntry({
uri: uri.toString(),
authorDid: event.did,
website: record.website,
note: record.note,
tags: record.tags,
createdAt: new Date(record.createdAt),
indexedAt: new Date(),
});
} else if (event.collection === com.sofiaritz.everest.graph.follow.$type) {
const record = tryParseRecord(
event.record, com.sofiaritz.everest.graph.follow.$parse,
);
if (!record) return NextResponse.json({ success: false });
await upsertFollow({
uri: uri.toString(),
follower: event.did,
following: record.subject,
createdAt: new Date(record.createdAt),
indexedAt: new Date(),
});
}
} else if (event.action === "delete") {
console.log("[tap/webhook] |> event.action=delete for record", uri.toString());
if (event.collection === com.sofiaritz.everest.entry.$type) {
await deleteEntry(uri);
} else if (event.collection === com.sofiaritz.everest.graph.follow.$type) {
await deleteFollow(uri);
}
}
}
return NextResponse.json({ success: true });
}