diff --git a/src/app/api/admin/credentials/revoke/route.ts b/src/app/api/admin/credentials/revoke/route.ts
new file mode 100644
index 0000000..1c4d716
--- /dev/null
+++ b/src/app/api/admin/credentials/revoke/route.ts
@@ -0,0 +1,82 @@
+import { NextRequest, NextResponse } from "next/server";
+import { z } from "zod";
+import { verifySessionToken, COOKIE_NAME } from "@/lib/auth/session";
+import { getDb, schema } from "@/lib/db";
+import { eq } from "drizzle-orm";
+
+const revokeSchema = z.object({
+ credentialId: z.string().min(1, "Credential ID is required"),
+ reason: z.string().min(5, "Reason must be at least 5 characters long"),
+});
+
+export async function POST(request: NextRequest) {
+ const token = request.cookies.get(COOKIE_NAME)?.value;
+ if (!token) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const sessionUser = await verifySessionToken(token);
+ if (!sessionUser || sessionUser.role !== "admin") {
+ return NextResponse.json(
+ { error: "Forbidden. Administrative privileges required." },
+ { status: 403 }
+ );
+ }
+
+ try {
+ const body = await request.json();
+ const data = revokeSchema.parse(body);
+
+ const db = await getDb();
+
+ // Verify credential exists
+ const credRecords = await db
+ .select()
+ .from(schema.credentials)
+ .where(eq(schema.credentials.id, data.credentialId))
+ .limit(1);
+
+ if (credRecords.length === 0) {
+ return NextResponse.json({ error: "Credential not found" }, { status: 404 });
+ }
+
+ const cred = credRecords[0];
+
+ // Revoke credential
+ await db
+ .update(schema.credentials)
+ .set({
+ status: "revoked",
+ revokedReason: data.reason,
+ })
+ .where(eq(schema.credentials.id, data.credentialId));
+
+ // Create immutable audit log
+ await db.insert(schema.auditLogs).values({
+ id: `audit_${crypto.randomUUID()}`,
+ actorId: sessionUser.id,
+ action: "credential.revoked",
+ targetType: "credential",
+ targetId: data.credentialId,
+ metadata: {
+ revokedBy: sessionUser.githubUsername,
+ reason: data.reason,
+ targetUserId: cred.userId,
+ credentialType: cred.type,
+ },
+ });
+
+ return NextResponse.json({
+ success: true,
+ message: `Credential ${data.credentialId} has been revoked.`,
+ });
+ } catch (error: any) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json({ error: error.errors[0].message }, { status: 400 });
+ }
+ return NextResponse.json(
+ { error: error.message || "Failed to revoke credential" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/api/badges/credential/[id]/badge.svg/route.ts b/src/app/api/badges/credential/[id]/badge.svg/route.ts
new file mode 100644
index 0000000..da9345f
--- /dev/null
+++ b/src/app/api/badges/credential/[id]/badge.svg/route.ts
@@ -0,0 +1,37 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getDb, schema } from "@/lib/db";
+import { eq } from "drizzle-orm";
+import { generateCredentialSvgBadge } from "@/lib/credentials/badge";
+
+export async function GET(
+ _request: NextRequest,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ const { id } = await params;
+ const db = await getDb();
+
+ const credRecords = await db
+ .select()
+ .from(schema.credentials)
+ .where(eq(schema.credentials.id, id))
+ .limit(1);
+
+ let svg: string;
+ if (credRecords.length === 0) {
+ svg = generateCredentialSvgBadge("Unverified", "revoked");
+ } else {
+ const cred = credRecords[0];
+ svg = generateCredentialSvgBadge(
+ cred.title.split("—")[0].trim(),
+ cred.status as "active" | "revoked"
+ );
+ }
+
+ return new NextResponse(svg, {
+ status: 200,
+ headers: {
+ "Content-Type": "image/svg+xml; charset=utf-8",
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
+ },
+ });
+}
diff --git a/src/app/verify/[id]/BadgeSnippet.tsx b/src/app/verify/[id]/BadgeSnippet.tsx
new file mode 100644
index 0000000..ad8c8f9
--- /dev/null
+++ b/src/app/verify/[id]/BadgeSnippet.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import { useState } from "react";
+
+export function BadgeSnippet({
+ credentialId,
+ title,
+ appUrl,
+}: {
+ credentialId: string;
+ title: string;
+ appUrl: string;
+}) {
+ const [copied, setCopied] = useState(false);
+
+ const snippet = `[](${appUrl}/verify/${credentialId})`;
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(snippet);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
+
+
+ Embed on GitHub Profile README
+
+
+ Showcase this verified proof of work on your personal GitHub README.
+
+
+
+
+
+
+
+ {snippet}
+
+
+
+
+
Live Badge Preview:
+

+
+
+ );
+}
diff --git a/src/app/verify/[id]/RevokeButton.tsx b/src/app/verify/[id]/RevokeButton.tsx
new file mode 100644
index 0000000..414ff89
--- /dev/null
+++ b/src/app/verify/[id]/RevokeButton.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+
+export function RevokeButton({ credentialId }: { credentialId: string }) {
+ const router = useRouter();
+ const [isOpen, setIsOpen] = useState(false);
+ const [reason, setReason] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleRevoke = async () => {
+ if (!reason.trim() || reason.length < 5) {
+ setError("Please provide a legitimate justification (min 5 chars).");
+ return;
+ }
+
+ setSubmitting(true);
+ setError(null);
+
+ try {
+ const res = await fetch("/api/admin/credentials/revoke", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ credentialId, reason }),
+ });
+
+ const data = await res.json();
+ if (!res.ok) {
+ throw new Error(data.error || "Failed to revoke credential");
+ }
+
+ setIsOpen(false);
+ router.refresh();
+ } catch (err: any) {
+ setError(err.message);
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ if (!isOpen) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ Administrative Revocation Action
+
+
+ This action is permanent and will be logged in the immutable audit registry.
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
setReason(e.target.value)}
+ placeholder="Reason for revocation (e.g. PR reverted, plagiarized code)"
+ className="w-full rounded border border-red-500/30 bg-slate-900 px-3 py-1.5 text-xs text-white placeholder-slate-500 focus:outline-none font-mono"
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/verify/[id]/page.tsx b/src/app/verify/[id]/page.tsx
index adfe445..c660e2e 100644
--- a/src/app/verify/[id]/page.tsx
+++ b/src/app/verify/[id]/page.tsx
@@ -1,7 +1,11 @@
import { getDb, schema } from "@/lib/db";
import { eq } from "drizzle-orm";
import Link from "next/link";
+import { cookies } from "next/headers";
import type { Metadata } from "next";
+import { verifySessionToken, COOKIE_NAME } from "@/lib/auth/session";
+import { BadgeSnippet } from "./BadgeSnippet";
+import { RevokeButton } from "./RevokeButton";
export const dynamic = "force-dynamic";
@@ -23,6 +27,10 @@ export default async function VerifyCredentialPage({
params: Promise<{ id: string }>;
}) {
const { id } = await params;
+ const cookieStore = await cookies();
+ const token = cookieStore.get(COOKIE_NAME)?.value;
+ const sessionUser = token ? await verifySessionToken(token) : null;
+
const db = await getDb();
const credRecords = await db
@@ -67,6 +75,7 @@ export default async function VerifyCredentialPage({
const holder = holderUsers[0];
const evidence = credential.evidenceData as any;
const isRevoked = credential.status === "revoked";
+ const appUrl = process.env.APP_URL || "http://localhost:3000";
return (
@@ -74,7 +83,7 @@ export default async function VerifyCredentialPage({
@@ -83,7 +92,7 @@ export default async function VerifyCredentialPage({
+ {/* If Revoked Banner */}
+ {isRevoked && (
+
+
+ Notice of Revocation
+
+
+ {credential.revokedReason || "This credential was revoked by repository maintainers."}
+
+
+ )}
+
{/* Holder & Issuer */}
@@ -125,14 +146,12 @@ export default async function VerifyCredentialPage({
{holder?.displayName || holder?.githubUsername}
-
- @{holder?.githubUsername}
-
+ @{holder?.githubUsername} (Passport ↗)
+
@@ -192,7 +211,7 @@ export default async function VerifyCredentialPage({
-
+
Inspect PR on GitHub
→
+
+ {/* Admin safety control */}
+ {sessionUser?.role === "admin" && !isRevoked && (
+
+ )}
+ {/* Embeddable Badge Snippet */}
+ {!isRevoked && (
+
+ )}
+
{/* Anti-certificate mill disclaimer */}
TechNexusOrg credentials represent verifiable open-source engineering work. This record is linked to public GitHub contributions.
diff --git a/src/lib/credentials/badge.test.ts b/src/lib/credentials/badge.test.ts
new file mode 100644
index 0000000..d01531d
--- /dev/null
+++ b/src/lib/credentials/badge.test.ts
@@ -0,0 +1,36 @@
+import { describe, it, expect } from "vitest";
+import { generateCredentialSvgBadge } from "./badge";
+
+describe("Credential SVG Badge Generator", () => {
+ it("generates active credential badge with correct colors and text", () => {
+ const svg = generateCredentialSvgBadge("First PR Merged", "active");
+
+ expect(svg).toContain("