Skip to content

feat(api): add secret token guard to internal filesystem-writing API routes #79

Description

@natashaannn

User story

As a developer, I want the internal editor and camera API routes to require a secret token, so that they are not accidentally callable from a public deployment or by an external user with knowledge of the URL.

Background

Two API routes write to the local filesystem under public/:

  • POST /api/editor/save-transcript — writes transcript.json and transcript.doc.txt
  • POST /api/camera/save-profiles — writes camera-profiles.json

These routes exist only for the internal editing workflow running on localhost. When the app is deployed publicly, these routes must not be callable by anyone without the internal secret. A static Bearer token in the Authorization header is sufficient — no session cookies or user auth needed.

Acceptance criteria

Happy path

Given the internal editor or camera UI sends a request with Authorization: Bearer {INTERNAL_API_SECRET}
When the request arrives at /api/editor/save-transcript or /api/camera/save-profiles
Then the request proceeds normally and the file is saved

Error path

Given any request arrives at these routes WITHOUT the Authorization header or with a wrong token
When it hits the handler
Then the server returns 401 { error: "Unauthorized" } and no filesystem write occurs

Given INTERNAL_API_SECRET is not set in the environment
When the server starts (or first request is received)
Then an error is thrown: "INTERNAL_API_SECRET env var is required"

Out of scope

  • Public-facing routes (no token check on /api/generate-carousel, /api/transcribe, etc.)
  • Session-based auth for internal routes
  • Any changes to internal editor or camera UI beyond adding the Authorization header to fetch calls

Technical context

The token check should live in a shared helper:

// app/lib/requireInternalToken.ts
export function requireInternalToken(req: NextRequest): NextResponse | null

Returns a 401 NextResponse if token is missing or wrong, null if OK.
Reads the expected token from process.env.INTERNAL_API_SECRET.

Add INTERNAL_API_SECRET to .env.example.

Files affected:

Implementation details

  1. Create app/lib/requireInternalToken.ts:
const secret = process.env.INTERNAL_API_SECRET;
if (!secret) throw new Error('INTERNAL_API_SECRET env var is required');

export function requireInternalToken(req: NextRequest): NextResponse | null {
  const auth = req.headers.get('authorization');
  if (!auth || !timingSafeEqual(auth, `Bearer ${secret}`)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  return null;
}

Use crypto.timingSafeEqual or a constant-time comparison to prevent timing attacks.

  1. In each internal API route, add at the top of the handler:
const guard = requireInternalToken(request);
if (guard) return guard;
  1. In the internal UI pages, add Authorization header to fetch calls. Prefer a Next.js Server Action so INTERNAL_API_SECRET stays server-side and never appears in the client bundle. If a server action is not feasible, use NEXT_PUBLIC_INTERNAL_API_SECRET and document the tradeoff.

Additional test scenarios

  • A request with a valid token to a public route (e.g. /api/generate-carousel) is NOT affected
  • INTERNAL_API_SECRET containing special characters (spaces, symbols) is handled correctly
  • Empty string Authorization header returns 401

Hard constraints

  • Token comparison must be constant-time (use crypto.timingSafeEqual) to prevent timing attacks
  • INTERNAL_API_SECRET must not appear in client-side JavaScript if avoidable — prefer server actions
  • 401 response must be JSON, not HTML

Dependency issues

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions