diff --git a/src/app/api/metrics/ci/route.ts b/src/app/api/metrics/ci/route.ts index 617062ab..f287c31b 100644 --- a/src/app/api/metrics/ci/route.ts +++ b/src/app/api/metrics/ci/route.ts @@ -9,6 +9,7 @@ import { import { GITHUB_API } from "@/lib/github"; import { supabaseAdmin } from "@/lib/supabase"; import { resolveAppUser } from "@/lib/resolve-user"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -210,7 +211,8 @@ async function fetchCIAnalyticsForAccount( export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken || !session.githubLogin) { + const accessToken = await getGitHubAccessToken(req); + if (!accessToken || !session?.githubLogin) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -219,7 +221,7 @@ export async function GET(req: NextRequest) { if (!accountId) { try { const result = await fetchCIAnalyticsForAccount( - session.accessToken, + accessToken, session.githubLogin ); return Response.json(result); @@ -241,7 +243,7 @@ export async function GET(req: NextRequest) { if (accountId === "combined") { const accounts = await getAllAccounts( { - token: session.accessToken, + token: accessToken, githubId: session.githubId, githubLogin: session.githubLogin, }, @@ -264,7 +266,7 @@ export async function GET(req: NextRequest) { if (accountId === session.githubId) { try { const result = await fetchCIAnalyticsForAccount( - session.accessToken, + accessToken, session.githubLogin ); return Response.json(result); diff --git a/src/app/api/metrics/compare/route.ts b/src/app/api/metrics/compare/route.ts index 7842abe6..1da1105a 100644 --- a/src/app/api/metrics/compare/route.ts +++ b/src/app/api/metrics/compare/route.ts @@ -1,6 +1,7 @@ import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; import { authOptions } from "@/lib/auth"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -16,7 +17,8 @@ function toDateStr(d: Date): string { export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken || !session.githubLogin) { + const accessToken = await getGitHubAccessToken(req); + if (!accessToken || !session?.githubLogin) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -40,7 +42,7 @@ export async function GET(req: NextRequest) { // 1. Verify user exists const userRes = await fetch(`${GITHUB_API}/users/${username}`, { - headers: { Authorization: `Bearer ${session.accessToken}` }, + headers: { Authorization: `Bearer ${accessToken}` }, next: { revalidate: 3600 }, }); @@ -53,7 +55,7 @@ export async function GET(req: NextRequest) { const since90 = new Date(); since90.setDate(since90.getDate() - 90); const since90Str = since90.toISOString().slice(0, 10); - + const since30 = new Date(); since30.setDate(since30.getDate() - 30); const since30Str = since30.toISOString().slice(0, 10); @@ -62,7 +64,7 @@ export async function GET(req: NextRequest) { `${GITHUB_API}/search/commits?q=author:${username}+author-date:>=${since90Str}&per_page=100&sort=author-date&order=desc`, { headers: { - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${accessToken}`, Accept: "application/vnd.github+json", }, next: { revalidate: 3600 }, @@ -111,7 +113,7 @@ export async function GET(req: NextRequest) { // 3. Top Language from repos const reposRes = await fetch(`${GITHUB_API}/users/${username}/repos?per_page=100&sort=pushed`, { - headers: { Authorization: `Bearer ${session.accessToken}` }, + headers: { Authorization: `Bearer ${accessToken}` }, next: { revalidate: 3600 }, }); @@ -131,7 +133,7 @@ export async function GET(req: NextRequest) { const prsRes = await fetch( `${GITHUB_API}/search/issues?q=type:pr+author:${username}&per_page=1`, { - headers: { Authorization: `Bearer ${session.accessToken}` }, + headers: { Authorization: `Bearer ${accessToken}` }, next: { revalidate: 3600 }, } ); diff --git a/src/app/api/metrics/contributions/route.ts b/src/app/api/metrics/contributions/route.ts index 3a3f885e..b45bbeda 100644 --- a/src/app/api/metrics/contributions/route.ts +++ b/src/app/api/metrics/contributions/route.ts @@ -15,6 +15,7 @@ import { } from "@/lib/metrics-cache"; import { supabaseAdmin } from "@/lib/supabase"; import { resolveAppUser } from "@/lib/resolve-user"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -232,7 +233,8 @@ async function mergeGitLabContributions( export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken || !session.githubLogin) { + const accessToken = await getGitHubAccessToken(req); + if (!accessToken || !session?.githubLogin) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -249,7 +251,7 @@ export async function GET(req: NextRequest) { if (username) { try { const result = await fetchContributionsForAccount( - session.accessToken, + accessToken, username, days, { bypass, userId: session.githubId ?? session.githubLogin } @@ -263,7 +265,7 @@ export async function GET(req: NextRequest) { if (!accountId) { try { const result = await fetchContributionsForAccount( - session.accessToken, + accessToken, session.githubLogin, days, { bypass, userId: session.githubId ?? session.githubLogin } @@ -297,7 +299,7 @@ export async function GET(req: NextRequest) { if (accountId === "combined") { const accounts = await getAllAccounts( { - token: session.accessToken, + token: accessToken, githubId: session.githubId, githubLogin: session.githubLogin, }, @@ -341,7 +343,7 @@ export async function GET(req: NextRequest) { if (accountId === session.githubId) { try { const result = await fetchContributionsForAccount( - session.accessToken, + accessToken, session.githubLogin, days, { bypass, userId: session.githubId } diff --git a/src/app/api/metrics/issues/route.ts b/src/app/api/metrics/issues/route.ts index 7abdf26e..1e1fcf2f 100644 --- a/src/app/api/metrics/issues/route.ts +++ b/src/app/api/metrics/issues/route.ts @@ -1,18 +1,21 @@ import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; import { authOptions } from "@/lib/auth"; import { fetchIssuesMetrics } from "@/lib/github"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; -export async function GET() { +export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken) { + const accessToken = await getGitHubAccessToken(req); + if (!session?.githubId || !accessToken) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } try { - const metrics = await fetchIssuesMetrics(session.accessToken); + const metrics = await fetchIssuesMetrics(accessToken); return Response.json(metrics); } catch { return Response.json({ error: "GitHub API error" }, { status: 502 }); diff --git a/src/app/api/metrics/languages/route.ts b/src/app/api/metrics/languages/route.ts index ba5b495b..65b6ccca 100644 --- a/src/app/api/metrics/languages/route.ts +++ b/src/app/api/metrics/languages/route.ts @@ -1,5 +1,7 @@ import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; import { authOptions } from "@/lib/auth"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -9,14 +11,15 @@ interface RepoItem { repository: { full_name: string }; } -export async function GET() { +export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken || !session.githubLogin) { + const accessToken = await getGitHubAccessToken(req); + if (!session?.githubLogin || !accessToken) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } const headers = { - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${accessToken}`, Accept: "application/vnd.github+json", }; diff --git a/src/app/api/metrics/pinned-repos/route.ts b/src/app/api/metrics/pinned-repos/route.ts index a7de84ef..9e323b1e 100644 --- a/src/app/api/metrics/pinned-repos/route.ts +++ b/src/app/api/metrics/pinned-repos/route.ts @@ -1,5 +1,7 @@ import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; import { authOptions } from "@/lib/auth"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -34,9 +36,10 @@ const PINNED_REPOS_QUERY = ` } `; -export async function GET() { +export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken) { + const accessToken = await getGitHubAccessToken(req); + if (!session?.githubId || !accessToken) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -45,7 +48,7 @@ export async function GET() { method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ query: PINNED_REPOS_QUERY }), cache: "no-store", diff --git a/src/app/api/metrics/pr-breakdown/route.ts b/src/app/api/metrics/pr-breakdown/route.ts index f83fb197..0ac71404 100644 --- a/src/app/api/metrics/pr-breakdown/route.ts +++ b/src/app/api/metrics/pr-breakdown/route.ts @@ -1,5 +1,7 @@ import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; import { authOptions } from "@/lib/auth"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -13,9 +15,10 @@ interface PRItem { }; } -export async function GET() { +export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken) { + const accessToken = await getGitHubAccessToken(req); + if (!session?.githubId || !accessToken) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -23,7 +26,7 @@ export async function GET() { `${GITHUB_API}/search/issues?q=type:pr+author:@me&per_page=100`, { headers: { - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${accessToken}`, Accept: "application/vnd.github+json", }, cache: "no-store", diff --git a/src/app/api/metrics/pr-review-time/route.ts b/src/app/api/metrics/pr-review-time/route.ts index b7a8852d..66fd85b4 100644 --- a/src/app/api/metrics/pr-review-time/route.ts +++ b/src/app/api/metrics/pr-review-time/route.ts @@ -14,6 +14,7 @@ import { withMetricsCache, } from "@/lib/metrics-cache"; import { resolveAppUser } from "@/lib/resolve-user"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -229,8 +230,9 @@ function formatTrendWeeks(weeks: TrendWeek[]) { export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); + const accessToken = await getGitHubAccessToken(req); - if (!session?.accessToken) { + if (!accessToken) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -239,9 +241,9 @@ export async function GET(req: NextRequest) { if (!accountId) { try { - const result = await fetchPRReviewTrendForAccount(session.accessToken, { + const result = await fetchPRReviewTrendForAccount(accessToken, { bypass, - userId: session.githubId ?? session.githubLogin ?? "primary", + userId: session?.githubId ?? session?.githubLogin ?? "primary", }); return Response.json(formatTrendWeeks(result)); @@ -250,7 +252,7 @@ export async function GET(req: NextRequest) { } } - if (!session.githubId || !session.githubLogin) { + if (!session?.githubId || !session.githubLogin) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -263,7 +265,7 @@ export async function GET(req: NextRequest) { if (accountId === "combined") { const accounts = await getAllAccounts( { - token: session.accessToken, + token: accessToken, githubId: session.githubId, githubLogin: session.githubLogin, }, @@ -290,7 +292,7 @@ export async function GET(req: NextRequest) { const token = accountId === session.githubId - ? session.accessToken + ? accessToken : await getAccountToken(userRow.id, accountId); if (!token) { diff --git a/src/app/api/metrics/prs/route.ts b/src/app/api/metrics/prs/route.ts index 5c9f7554..4109d9fd 100644 --- a/src/app/api/metrics/prs/route.ts +++ b/src/app/api/metrics/prs/route.ts @@ -15,6 +15,7 @@ import { } from "@/lib/metrics-cache"; import { supabaseAdmin } from "@/lib/supabase"; import { resolveAppUser } from "@/lib/resolve-user"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -236,7 +237,8 @@ function formatPRMetrics(metrics: PRMetricsBase) { export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken) { + const accessToken = await getGitHubAccessToken(req); + if (!accessToken) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -245,9 +247,9 @@ export async function GET(req: NextRequest) { if (!accountId) { try { - const result = await fetchCachedPRMetrics(session.accessToken, { + const result = await fetchCachedPRMetrics(accessToken, { bypass, - userId: session.githubId ?? session.githubLogin ?? "primary", + userId: session?.githubId ?? session?.githubLogin ?? "primary", }); return Response.json(formatPRMetrics(result)); } catch { @@ -255,7 +257,7 @@ export async function GET(req: NextRequest) { } } - if (!session.githubId || !session.githubLogin) { + if (!session?.githubId || !session.githubLogin) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -268,7 +270,7 @@ export async function GET(req: NextRequest) { if (accountId === "combined") { const accounts = await getAllAccounts( { - token: session.accessToken, + token: accessToken, githubId: session.githubId, githubLogin: session.githubLogin, }, @@ -323,7 +325,7 @@ export async function GET(req: NextRequest) { const token = accountId === session.githubId - ? session.accessToken + ? accessToken : await getAccountToken(userRow.id, accountId); if (!token) { diff --git a/src/app/api/metrics/repo-health/route.ts b/src/app/api/metrics/repo-health/route.ts index 75a6a7e6..7e4f0c38 100644 --- a/src/app/api/metrics/repo-health/route.ts +++ b/src/app/api/metrics/repo-health/route.ts @@ -3,6 +3,7 @@ import { NextRequest } from "next/server"; import { authOptions } from "@/lib/auth"; import { computeHealthScore } from "@/lib/repo-health"; import type { RepoHealthResponse, RepoHealthSignals, RepoHealthScore } from "@/types/repo-health"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -167,7 +168,8 @@ async function fetchSignalsForRepo( export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken || !session.githubLogin) { + const accessToken = await getGitHubAccessToken(req); + if (!accessToken || !session?.githubLogin) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -180,7 +182,7 @@ export async function GET(req: NextRequest) { // 1) Determine top repos (top 6 by commit count). let topRepos: RepoSummary[] = []; try { - topRepos = (await fetchReposForAccount(session.accessToken, session.githubLogin, days)).repos; + topRepos = (await fetchReposForAccount(accessToken, session.githubLogin, days)).repos; } catch { return Response.json({ error: "GitHub API error" }, { status: 502 }); } @@ -190,7 +192,7 @@ export async function GET(req: NextRequest) { // 2) Fetch per-repo signals sequentially to preserve rate limits. for (const repo of topRepos) { try { - const signals = await fetchSignalsForRepo(session.accessToken, repo.name, days); + const signals = await fetchSignalsForRepo(accessToken, repo.name, days); scores.push(computeHealthScore(repo.name, signals)); } catch { // Skip repo on any failure. diff --git a/src/app/api/metrics/repos/route.ts b/src/app/api/metrics/repos/route.ts index ee3fd1cb..4c99308f 100644 --- a/src/app/api/metrics/repos/route.ts +++ b/src/app/api/metrics/repos/route.ts @@ -15,6 +15,7 @@ import { } from "@/lib/metrics-cache"; import { supabaseAdmin } from "@/lib/supabase"; import { resolveAppUser } from "@/lib/resolve-user"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -168,7 +169,8 @@ async function fetchReposForAccount( export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken || !session.githubLogin) { + const accessToken = await getGitHubAccessToken(req); + if (!accessToken || !session?.githubLogin) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -181,7 +183,7 @@ export async function GET(req: NextRequest) { if (!accountId) { try { const result = await fetchReposForAccount( - session.accessToken, + accessToken, session.githubLogin, days, { bypass, userId: session.githubId ?? session.githubLogin } @@ -205,7 +207,7 @@ export async function GET(req: NextRequest) { if (accountId === "combined") { const accounts = await getAllAccounts( { - token: session.accessToken, + token: accessToken, githubId: session.githubId, githubLogin: session.githubLogin, }, @@ -236,7 +238,7 @@ export async function GET(req: NextRequest) { if (accountId === session.githubId) { try { const result = await fetchReposForAccount( - session.accessToken, + accessToken, session.githubLogin, days, { bypass, userId: session.githubId } diff --git a/src/app/api/metrics/streak/route.ts b/src/app/api/metrics/streak/route.ts index 3d0045c9..93531c17 100644 --- a/src/app/api/metrics/streak/route.ts +++ b/src/app/api/metrics/streak/route.ts @@ -11,6 +11,7 @@ import { } from "@/lib/metrics-cache"; import { supabaseAdmin } from "@/lib/supabase"; import { resolveAppUser } from "@/lib/resolve-user"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -144,7 +145,8 @@ function calculateStreakFromDates( export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken || !session.githubLogin || !session.githubId) { + const accessToken = await getGitHubAccessToken(req); + if (!accessToken || !session?.githubLogin || !session.githubId) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -182,7 +184,7 @@ export async function GET(req: NextRequest) { try { const activeDates = await fetchActiveDates( session.githubLogin, - session.accessToken, + accessToken, { bypass, userId: session.githubId } ); return Response.json( @@ -200,7 +202,7 @@ export async function GET(req: NextRequest) { if (accountId === "combined") { const accounts = await getAllAccounts( { - token: session.accessToken, + token: accessToken, githubId: session.githubId, githubLogin: session.githubLogin, }, @@ -226,7 +228,7 @@ export async function GET(req: NextRequest) { return Response.json(calculateStreakFromDates(unifiedDates, freezeDates)); } - let resolvedToken = session.accessToken; + let resolvedToken = accessToken; let resolvedLogin = session.githubLogin; if (accountId !== session.githubId) { diff --git a/src/app/api/metrics/weekly-summary/route.ts b/src/app/api/metrics/weekly-summary/route.ts index 19d9d242..96b83f7c 100644 --- a/src/app/api/metrics/weekly-summary/route.ts +++ b/src/app/api/metrics/weekly-summary/route.ts @@ -1,6 +1,8 @@ import { getServerSession } from "next-auth"; +import { NextRequest } from "next/server"; import { authOptions } from "@/lib/auth"; import { GITHUB_API } from "@/lib/github"; +import { getGitHubAccessToken } from "@/lib/server-github-token"; export const dynamic = "force-dynamic"; @@ -95,9 +97,10 @@ async function fetchActiveDates( return activeDates; } -export async function GET() { +export async function GET(req: NextRequest) { const session = await getServerSession(authOptions); - if (!session?.accessToken || !session.githubLogin) { + const accessToken = await getGitHubAccessToken(req); + if (!session?.githubLogin || !accessToken) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } @@ -112,7 +115,7 @@ export async function GET() { `${GITHUB_API}/search/commits?q=author:${session.githubLogin}+author-date:>=${fourteenDaysAgoStr}&per_page=100`, { headers: { - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${accessToken}`, Accept: "application/vnd.github.cloak-preview+json", }, cache: "no-store", @@ -162,7 +165,7 @@ export async function GET() { `${GITHUB_API}/search/issues?q=type:pr+author:@me+created:>=${fourteenDaysAgoStr}&per_page=100`, { headers: { - Authorization: `Bearer ${session.accessToken}`, + Authorization: `Bearer ${accessToken}`, Accept: "application/vnd.github+json", }, cache: "no-store", @@ -197,7 +200,7 @@ export async function GET() { const streakDates = await fetchActiveDates( session.githubLogin, - session.accessToken + accessToken ); const currentStreak = calculateCurrentStreak(streakDates); const commitDelta = commitsThisWeek - commitsPrevWeek; diff --git a/src/lib/auth.ts b/src/lib/auth.ts index fb3f4210..9e3d056d 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -63,8 +63,9 @@ export const authOptions: NextAuthOptions = { return token; }, async session({ session, token }) { - if (typeof token.accessToken === "string") - session.accessToken = token.accessToken; + // accessToken is intentionally NOT copied here — it must never be sent + // to the browser via /api/auth/session. Retrieve it server-side only + // using getGitHubAccessToken(req) from @/lib/server-github-token. if (typeof token.githubId === "string") session.githubId = token.githubId; if (typeof token.githubLogin === "string") diff --git a/src/lib/server-github-token.ts b/src/lib/server-github-token.ts new file mode 100644 index 00000000..3bd9b23a --- /dev/null +++ b/src/lib/server-github-token.ts @@ -0,0 +1,13 @@ +import { getToken } from "next-auth/jwt"; +import type { NextRequest } from "next/server"; + +/** + * Retrieves the GitHub OAuth access token from the server-side JWT (httpOnly cookie). + * The token is never exposed to the browser — call this only in server-side API routes. + */ +export async function getGitHubAccessToken( + req: NextRequest +): Promise { + const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET }); + return typeof token?.accessToken === "string" ? token.accessToken : null; +} diff --git a/src/lib/validate-github-username.ts b/src/lib/validate-github-username.ts new file mode 100644 index 00000000..f818656d --- /dev/null +++ b/src/lib/validate-github-username.ts @@ -0,0 +1,11 @@ +/** + * Validates a GitHub username against GitHub's official format rules: + * 1–39 characters, alphanumeric and hyphens only, cannot start or end with a hyphen. + * + * Rejecting special characters (+, /, ?, &, etc.) prevents injection of additional + * GitHub search qualifiers or URL path segments into server-side GitHub API calls. + */ +export function isValidGitHubUsername(username: unknown): username is string { + if (typeof username !== "string") return false; + return /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(username); +} diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts index 6fba03ba..452aa88a 100644 --- a/src/types/next-auth.d.ts +++ b/src/types/next-auth.d.ts @@ -2,7 +2,8 @@ import type { DefaultSession } from "next-auth"; declare module "next-auth" { interface Session { - accessToken?: string; + // accessToken is deliberately absent — it lives only in the server-side + // JWT (httpOnly cookie). Use getGitHubAccessToken(req) in API routes. githubId?: string; githubLogin?: string; gitlabToken?: string;