diff --git a/src/middleware/adminAuth.ts b/src/middleware/adminAuth.ts index bc48965..55e7846 100644 --- a/src/middleware/adminAuth.ts +++ b/src/middleware/adminAuth.ts @@ -1,5 +1,6 @@ import crypto from 'node:crypto' import type { Context, Next } from 'hono' +import { hasValidReviewerSession } from './reviewerSession.js' /** * Shared admin authentication middleware. @@ -26,7 +27,10 @@ export async function adminAuth(c: Context, next: Next) { return c.json({ error: 'Admin key not configured' }, 503) } - if (!hasValidAdminKey(c.req.header('x-admin-key'))) { + const headerAuthorized = hasValidAdminKey(c.req.header('x-admin-key')) + const reviewerSessionAuthorized = headerAuthorized ? false : await hasValidReviewerSession(c) + + if (!headerAuthorized && !reviewerSessionAuthorized) { return c.json({ error: 'Unauthorized' }, 401) } await next() diff --git a/src/middleware/reviewerSession.ts b/src/middleware/reviewerSession.ts new file mode 100644 index 0000000..62e019a --- /dev/null +++ b/src/middleware/reviewerSession.ts @@ -0,0 +1,39 @@ +import type { Context } from 'hono' +import { deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie' + +export const REVIEWER_SESSION_COOKIE = 'djd_reviewer_session' +export const REVIEWER_SESSION_MAX_AGE_SECONDS = 60 * 60 * 8 +const REVIEWER_SESSION_VALUE = 'reviewer' + +function getReviewerSessionCookieOptions() { + return { + httpOnly: true, + maxAge: REVIEWER_SESSION_MAX_AGE_SECONDS, + path: '/', + sameSite: 'Strict' as const, + secure: process.env.NODE_ENV === 'production', + } +} + +export async function hasValidReviewerSession(c: Context): Promise { + const adminKey = process.env.ADMIN_KEY + if (!adminKey) { + return false + } + + const session = await getSignedCookie(c, adminKey, REVIEWER_SESSION_COOKIE) + return session === REVIEWER_SESSION_VALUE +} + +export async function startReviewerSession(c: Context): Promise { + const adminKey = process.env.ADMIN_KEY + if (!adminKey) { + return + } + + await setSignedCookie(c, REVIEWER_SESSION_COOKIE, REVIEWER_SESSION_VALUE, adminKey, getReviewerSessionCookieOptions()) +} + +export function clearReviewerSession(c: Context): void { + deleteCookie(c, REVIEWER_SESSION_COOKIE, getReviewerSessionCookieOptions()) +} diff --git a/src/routes/reviewer.ts b/src/routes/reviewer.ts index 5377c3b..6cacebb 100644 --- a/src/routes/reviewer.ts +++ b/src/routes/reviewer.ts @@ -1,12 +1,63 @@ import { Hono } from 'hono' +import { ErrorCodes, errorResponse } from '../errors.js' +import { hasValidAdminKey } from '../middleware/adminAuth.js' +import { + clearReviewerSession, + hasValidReviewerSession, + REVIEWER_SESSION_MAX_AGE_SECONDS, + startReviewerSession, +} from '../middleware/reviewerSession.js' import { reviewerPageHtml } from '../templates/reviewer.js' const reviewer = new Hono() +reviewer.use('*', async (c, next) => { + c.header('Cache-Control', 'no-store') + await next() +}) + reviewer.get('/', (c) => { c.header('Content-Type', 'text/html; charset=utf-8') - c.header('Cache-Control', 'no-store') return c.body(reviewerPageHtml()) }) +reviewer.get('/session', async (c) => { + const authenticated = await hasValidReviewerSession(c) + return c.json({ + authenticated, + expires_in_seconds: authenticated ? REVIEWER_SESSION_MAX_AGE_SECONDS : 0, + }) +}) + +reviewer.post('/session', async (c) => { + if (!process.env.ADMIN_KEY) { + return c.json({ error: 'Admin key not configured' }, 503) + } + + const body = await c.req.json<{ admin_key?: string }>().catch(() => null) + if (!body) { + return c.json(errorResponse(ErrorCodes.INVALID_JSON, 'Invalid JSON body'), 400) + } + + if (!hasValidAdminKey(body.admin_key)) { + return c.json(errorResponse('unauthorized', 'Unauthorized'), 401) + } + + await startReviewerSession(c) + return c.json({ + authenticated: true, + expires_in_seconds: REVIEWER_SESSION_MAX_AGE_SECONDS, + message: 'Reviewer session started.', + }) +}) + +reviewer.delete('/session', (c) => { + clearReviewerSession(c) + return c.json({ + authenticated: false, + expires_in_seconds: 0, + message: 'Reviewer session ended.', + }) +}) + export default reviewer diff --git a/src/templates/legal.ts b/src/templates/legal.ts index dee7edc..3b0b017 100644 --- a/src/templates/legal.ts +++ b/src/templates/legal.ts @@ -127,7 +127,7 @@ export const tosContent = ` export const privacyContent = `

DJD AGENT SCORE

PRIVACY POLICY

-

Last Updated: February 20, 2026

+

Last Updated: March 14, 2026

This Privacy Policy describes how DJD Agent Score (“Company,” “we,” “our”) collects, uses, stores, and discloses information in connection with the DJD Agent Score API and related services (the “Service”). By using the Service, you acknowledge and agree to this Privacy Policy.

@@ -157,8 +157,11 @@ export const privacyContent = `

2.3 User-Submitted Data

When you submit fraud reports, ratings, or other content through the Service, we collect the content of your submission, the submitting wallet address, the target wallet address, supporting evidence (transaction hashes), and timestamps. All user-submitted data is voluntary.

-

2.4 Information We Do NOT Collect

-

We do not collect names, email addresses, phone numbers, physical addresses, government identification numbers, biometric data, or any traditional PII. We do not use cookies, tracking pixels, or browser fingerprinting. We do not integrate with advertising networks. The Service does not have user accounts, logins, or passwords.

+

2.4 Reviewer Session Data

+

The internal reviewer dashboard uses a short-lived, signed HttpOnly session cookie to authenticate administrative review actions. This cookie is not used for advertising, analytics, or cross-site tracking. It exists solely to protect internal certification review workflows and expires automatically.

+ +

2.5 Information We Do NOT Collect

+

We do not collect names, email addresses, phone numbers, physical addresses, government identification numbers, biometric data, or any traditional PII. We do not use tracking pixels or browser fingerprinting. We do not integrate with advertising networks. The Service does not have consumer-facing user accounts, social profiles, or password-based logins.

3. How We Use Information

Service delivery: Calculating reputation scores, processing fraud reports, generating leaderboards, and serving API responses.

diff --git a/src/templates/reviewer.ts b/src/templates/reviewer.ts index be60b0f..bb9f609 100644 --- a/src/templates/reviewer.ts +++ b/src/templates/reviewer.ts @@ -1,3 +1,7 @@ +import { REVIEWER_SESSION_MAX_AGE_SECONDS } from '../middleware/reviewerSession.js' + +const REVIEWER_SESSION_HOURS = Math.floor(REVIEWER_SESSION_MAX_AGE_SECONDS / 3600) + export function reviewerPageHtml(): string { return ` @@ -112,7 +116,7 @@ export function reviewerPageHtml(): string {

Certification Reviewer Dashboard

-

Internal operations surface for DJD Certify. Load the review queue with an admin key, inspect score and profile context, then approve, request more information, reject, or issue a certification from an approved review.

+

Internal operations surface for DJD Certify. Start a short-lived reviewer session with the admin key, inspect score and profile context, then approve, request more information, reject, or issue a certification from an approved review.

@@ -145,10 +149,12 @@ export function reviewerPageHtml(): string {
+ +
-
This dashboard keeps the admin key and queue filters in sessionStorage for the current tab only.
+
Reviewer authentication uses a signed HttpOnly cookie for up to ${REVIEWER_SESSION_HOURS} hours. Only queue filters stay in sessionStorage for the current tab.
@@ -162,9 +168,11 @@ export function reviewerPageHtml(): string {