From 71bc3fa4f01b308cb7d20fcf8b64e74a015ae54e Mon Sep 17 00:00:00 2001 From: dev-agent Date: Tue, 26 May 2026 06:39:24 +0000 Subject: [PATCH 1/2] [#1244] Add /activation-status endpoint (public read) GET /api/airdrop/activation-status?address=X returns x_handle_confirmed_at, x_follow_at, fc_verified_at, activated_at. All-null for non-existent address (no 404). Cache-Control: no-store for refresh-resume safety (R20). No auth, no write side effects. 4 tests cover activated wallet, non-existent, cache header, missing param. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- .../airdrop/activation-status/route.test.ts | 63 +++++++++++++++++++ .../api/airdrop/activation-status/route.ts | 32 ++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/app/api/airdrop/activation-status/route.test.ts create mode 100644 src/app/api/airdrop/activation-status/route.ts diff --git a/package.json b/package.json index 6f10b0d..211044a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "1.32.1", + "version": "1.32.2", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/api/airdrop/activation-status/route.test.ts b/src/app/api/airdrop/activation-status/route.test.ts new file mode 100644 index 0000000..f3bd56c --- /dev/null +++ b/src/app/api/airdrop/activation-status/route.test.ts @@ -0,0 +1,63 @@ +// @vitest-environment node +import { describe, expect, it, vi } from "vitest"; + +const mockSingle = vi.fn(); + +vi.mock("../../../../../lib/supabase", () => ({ + createServerClient: () => ({ + from: () => ({ + select: () => ({ eq: () => ({ single: mockSingle }) }), + }), + }), +})); + +import { GET } from "./route"; + +function makeReq(address?: string) { + const url = address + ? `http://localhost/api/airdrop/activation-status?address=${address}` + : "http://localhost/api/airdrop/activation-status"; + return new Request(url); +} + +describe("GET /api/airdrop/activation-status", () => { + it("returns 4 fields for activated wallet", async () => { + mockSingle.mockResolvedValue({ + data: { + x_handle_confirmed_at: "2026-07-01", + x_follow_at: "2026-07-02", + fc_verified_at: "2026-07-03", + activated_at: "2026-07-02", + }, + }); + const res = await GET(makeReq("0xabc")); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data.x_handle_confirmed_at).toBe("2026-07-01"); + expect(data.activated_at).toBe("2026-07-02"); + }); + + it("returns all-null for non-existent address", async () => { + mockSingle.mockResolvedValue({ data: null }); + const res = await GET(makeReq("0xnonexistent")); + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toEqual({ + x_handle_confirmed_at: null, + x_follow_at: null, + fc_verified_at: null, + activated_at: null, + }); + }); + + it("includes Cache-Control: no-store header", async () => { + mockSingle.mockResolvedValue({ data: null }); + const res = await GET(makeReq("0xabc")); + expect(res.headers.get("Cache-Control")).toBe("no-store"); + }); + + it("returns 400 when address is missing", async () => { + const res = await GET(makeReq()); + expect(res.status).toBe(400); + }); +}); diff --git a/src/app/api/airdrop/activation-status/route.ts b/src/app/api/airdrop/activation-status/route.ts new file mode 100644 index 0000000..f5283d2 --- /dev/null +++ b/src/app/api/airdrop/activation-status/route.ts @@ -0,0 +1,32 @@ +import { NextResponse } from "next/server"; +import { createServerClient } from "../../../../../lib/supabase"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const address = searchParams.get("address")?.toLowerCase(); + + if (!address) { + return NextResponse.json({ error: "address parameter required" }, { status: 400 }); + } + + const supabase = createServerClient(); + if (!supabase) { + return NextResponse.json({ error: "Supabase not configured" }, { status: 500 }); + } + + const { data } = await supabase + .from("pl_activations") + .select("x_handle_confirmed_at, x_follow_at, fc_verified_at, activated_at") + .eq("address", address) + .single(); + + return NextResponse.json( + { + x_handle_confirmed_at: data?.x_handle_confirmed_at ?? null, + x_follow_at: data?.x_follow_at ?? null, + fc_verified_at: data?.fc_verified_at ?? null, + activated_at: data?.activated_at ?? null, + }, + { headers: { "Cache-Control": "no-store" } }, + ); +} From 8bbfb37179941901f7525cd68e54e316ce8dee6f Mon Sep 17 00:00:00 2001 From: dev-agent Date: Tue, 26 May 2026 06:40:46 +0000 Subject: [PATCH 2/2] [#1244] Sync lockfile version to 1.32.2 Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37db427..97ad5f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plotlink", - "version": "1.32.1", + "version": "1.32.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plotlink", - "version": "1.32.1", + "version": "1.32.2", "workspaces": [ "packages/*" ],