diff --git a/app/(app)/dashboard/settings/actors/page.tsx b/app/(app)/dashboard/settings/actors/page.tsx new file mode 100644 index 00000000..4ccda1d8 --- /dev/null +++ b/app/(app)/dashboard/settings/actors/page.tsx @@ -0,0 +1,53 @@ +import Link from "next/link"; +import { createClient } from "@/lib/supabase/server"; +import { serviceClient } from "@/lib/supabase/service"; +import { listActors } from "@/lib/tracker/actorStore"; +import { DECLARED_DEFINITION } from "@/lib/tracker/actors"; +import { ActorsPanel } from "./panel"; + +export const metadata = { title: "Declared actors" }; +export const dynamic = "force-dynamic"; + +export default async function ActorsPage() { + const supabase = await createClient(); + const { + data: { user }, + } = await supabase.auth.getUser(); + const sb = serviceClient(); + const [res, { data: projects }] = await Promise.all([ + listActors(sb, user!.id), + // The sites a browser declaration links to: yours, with the tracker on. + sb + .from("projects") + .select("id, name, url") + .eq("owner_id", user!.id) + .eq("tracker_enabled", true) + .order("name", { ascending: true }), + ]); + + return ( +
+ Say who you are, and whether you are a person or an agent, on every site running the + CrawlProof tracker. {DECLARED_DEFINITION} Names are visible to you only unless you make an + actor public; other site owners see counts. +
++ Open each site once in this browser. The tracker stores the token for that site and + removes it from the address bar; every later visit is declared. The tracker stays + cookieless, so this is per site and per browser. +
+ {sites.length > 0 ? ( +None of your projects has the tracker on yet.
+ )} +
+ Any other site with the tracker: drag this to your bookmarks bar and click it there:{" "}
+
{freshToken}
+
+ Send it as the Crawlproof-Actor header (headless agents), open any tracked
+ site with ?crp_actor=<token>, or call{" "}
+ crawlproof('actor', '<token>').
+
{error}
} + {notice &&{notice}
} + +No actors yet. Add yourself first.
+ )} + {actors.map((a) => { + const op = a.operator_actor_id ? actors.find((x) => x.id === a.operator_actor_id) : null; + return ( ++ {a.name || a.email}{" "} + {a.kind} +
++ {a.email} · {a.email_verified ? "verified" : "unverified"} · {a.visibility} + {op ? ` · operated by ${op.name || op.email}` : ""} +
++ Last 30 days: {a.last30.pageviews} pageviews, {a.last30.events} events on{" "} + {a.last30.sites} site{a.last30.sites === 1 ? "" : "s"} + {a.last30.contradictions ? ( + + {" "}· {a.last30.contradictions} contradicted by bot detection + + ) : null} +
+{t.prefix}…
+ {t.label || "(no label)"}
+ last used {ago(t.last_used_at)}
+
+ Declared actors
++ Tell tracked sites who you are, and whether you are a + person or an agent. Opt-in, self-reported. +
+
+ Nothing on the wire separates a person from an agent driving a real
+ browser, so visitors may say who they are. A CrawlProof account
+ registers actors (an email and a kind: human or agent) and mints a{" "}
+ cpa_ token for each browser or
+ agent. The token is the credential; an email alone claims nothing.
+
{`# an agent: send the header on every request
+Crawlproof-Actor: cpa_…
+
+# a person: open each site once (stored for that site, removed from the URL)
+https://example.com/?crp_actor=cpa_…
+
+# or from page code
+window.crawlproof?.("actor", "cpa_…"); // null forgets it`}
+
+ It is self-reported, so the rule is one-way: a declared agent is
+ believed and counted as a bot; a declared human is recorded but never
+ overrides bot detection, and a mismatch is counted as a contradiction
+ against that actor. Names are visible only to the actor's owner
+ unless made public; other site owners see per-kind counts. Manage
+ actors under Settings → Declared actors, or with{" "}
+ crawlproof actors.
+
@@ -217,10 +245,10 @@ window.crawlproof?.track("purchase", "pro_plan");`}
sessionStorage{" "}
- when available and reset when the tab session ends.
+ A random visitor id and a session id (30 minutes of inactivity)
+ live in this site's localStorage,
+ plus a declared-actor token only for visitors who opt in.
${esc(body)}
` + + ``, + { status, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } }, + ); +} + +export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string }> }) { + const { id } = await ctx.params; + const res = await verifyActorEmail(serviceClient(), id, req.nextUrl.searchParams.get("t") ?? ""); + if (!res.ok) return page("Not verified", res.error, res.status); + return page("Address verified", `${res.value.email} is now a verified declared actor.`, 200); +} diff --git a/app/api/tracker/v1/actors/route.ts b/app/api/tracker/v1/actors/route.ts new file mode 100644 index 00000000..c58f776f --- /dev/null +++ b/app/api/tracker/v1/actors/route.ts @@ -0,0 +1,40 @@ +// /api/tracker/v1/actors — declared actors (lib/tracker/actors.ts). +// +// GET list this account's actors, tokens, 30-day use +// POST {email, kind, name?, operator?, visibility?, token_label?} +// register one; token_label also mints a token, +// returned ONCE in `token` +// +// Auth: Bearer crp_… or a dashboard session. + +import { NextResponse, type NextRequest } from "next/server"; +import { serviceClient } from "@/lib/supabase/service"; +import { actorOwner } from "@/lib/tracker/actorAuth"; +import { createActor, listActors } from "@/lib/tracker/actorStore"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +export async function GET(req: NextRequest) { + const owner = await actorOwner(req); + if (!owner.ok) return NextResponse.json({ error: owner.error }, { status: owner.status }); + const res = await listActors(serviceClient(), owner.userId); + if (!res.ok) return NextResponse.json({ error: res.error }, { status: res.status }); + return NextResponse.json({ actors: res.value }); +} + +export async function POST(req: NextRequest) { + const owner = await actorOwner(req); + if (!owner.ok) return NextResponse.json({ error: owner.error }, { status: owner.status }); + const body = (await req.json().catch(() => ({}))) as Record+ A CrawlProof account registered this address${label}, so that + visits it declares are counted as ${who} on sites using the + CrawlProof tracker. Confirming marks the address verified; + until then it is shown as unverified. +
+
+ Or copy this link into your browser:
+ ${input.verifyUrl}
+