Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/middleware/adminAuth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import crypto from 'node:crypto'
import type { Context, Next } from 'hono'
import { hasValidReviewerSession } from './reviewerSession.js'

/**
* Shared admin authentication middleware.
Expand All @@ -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()
Expand Down
39 changes: 39 additions & 0 deletions src/middleware/reviewerSession.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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<void> {
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())
}
53 changes: 52 additions & 1 deletion src/routes/reviewer.ts
Original file line number Diff line number Diff line change
@@ -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
9 changes: 6 additions & 3 deletions src/templates/legal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export const tosContent = `
export const privacyContent = `
<h1 class="title">DJD AGENT SCORE</h1>
<h1 class="subtitle">PRIVACY POLICY</h1>
<p class="date">Last Updated: February 20, 2026</p>
<p class="date">Last Updated: March 14, 2026</p>

<p>This Privacy Policy describes how DJD Agent Score (&ldquo;<strong>Company,</strong>&rdquo; &ldquo;<strong>we,</strong>&rdquo; &ldquo;<strong>our</strong>&rdquo;) collects, uses, stores, and discloses information in connection with the DJD Agent Score API and related services (the &ldquo;<strong>Service</strong>&rdquo;). By using the Service, you acknowledge and agree to this Privacy Policy.</p>

Expand Down Expand Up @@ -157,8 +157,11 @@ export const privacyContent = `
<h2>2.3 User-Submitted Data</h2>
<p>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.</p>

<h2>2.4 Information We Do NOT Collect</h2>
<p>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.</p>
<h2>2.4 Reviewer Session Data</h2>
<p>The internal reviewer dashboard uses a short-lived, signed <code>HttpOnly</code> 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.</p>

<h2>2.5 Information We Do NOT Collect</h2>
<p>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.</p>

<h1>3. How We Use Information</h1>
<p><strong>Service delivery:</strong> Calculating reputation scores, processing fraud reports, generating leaderboards, and serving API responses.</p>
Expand Down
Loading