ATHELOS
Reference

API

Server actions and API routes for programmatic access.

API

ATHELOS is primarily used through its UI, but exposes server actions and API routes for automation and integration.

Two access paths

Server actions — called from within the app itself. Not exposed to the network. Type-safe, session-authenticated.

API routes — HTTP endpoints under /api. Callable by scripts, webhooks, or external tools. Session-authenticated via cookies.

Neither is designed for public consumption. Both assume you're behind a VPN or on the local machine.

Server actions

Server actions live in apps/web/src/lib/*/actions.ts and are marked "use server". They're called from client components via React's server action mechanism.

Every action:

  1. Requires a valid session (requireUser())
  2. Validates input with Zod
  3. Performs the operation
  4. Writes to the audit log
  5. Revalidates affected paths
  6. Returns { ok: true, ... } or { ok: false, error }

Actions never throw across the boundary. Errors are returned, not raised.

Action groups

Knowledge base — lib/kb/actions.ts

ActionPurpose
createArticleCreate a KB article
updateArticleUpdate an existing article
deleteArticleDelete an article

Playbooks — lib/playbooks/actions.ts

ActionPurpose
loadPlaybookAttach a playbook to an engagement
toggleTaskMark a task complete or incomplete
updateTaskNotesUpdate notes on a task
attachFindingToTaskLink a finding to a task
attachEvidenceToTaskLink evidence to a task
detachFindingFromTaskUnlink
detachEvidenceFromTaskUnlink
removePlaybookRemove a playbook from an engagement

Engagements — lib/engagements/actions.ts

ActionPurpose
createEngagementCreate a new engagement
updateEngagementUpdate fields
advancePhaseMove the lifecycle forward
revertPhaseMove the lifecycle back

Findings — lib/findings/actions.ts

ActionPurpose
createFindingCreate a new finding
updateFindingUpdate fields
deleteFindingDelete
setFindingStatusChange status

Evidence — lib/evidence/actions.ts

ActionPurpose
uploadEvidenceAttach a file to a finding
deleteEvidenceRemove

Reports — lib/reports/actions.ts

ActionPurpose
generateReportGenerate from engagement
updateReportFieldUpdate a text field
updateReportSectionUpdate a JSONB section
duplicateReportCreate a new version
setReportStatusChange status

Deliveries — lib/deliveries/actions.ts

ActionPurpose
createDeliveryCreate a delivery link
revokeDeliveryRevoke a link
recordViewLog a client view (public)

Clients — lib/clients/actions.ts

ActionPurpose
createClientCreate a client
updateClientUpdate
addContactAdd a contact
logInteractionLog a touchpoint

Invoices — lib/invoices/actions.ts

ActionPurpose
createInvoiceCreate an invoice
addLineItemAdd a line item
recordPaymentRecord payment received
cancelInvoiceVoid

AI — lib/ai/actions.ts

ActionPurpose
embedArticleGenerate an embedding
removeArticleEmbeddingRemove an embedding
embedAllArticlesReindex everything
chatWithKbRAG chat with streaming
draftExecSummaryAssistive action
suggestRemediationAssistive action
mapToAttckAssistive action

Notifications — lib/notify/actions.ts

ActionPurpose
sendTestEmailSend a test email
sendTestWebhookSend a test webhook
updateNotificationSettingsUpdate preferences

Auth — lib/auth/*

ActionPurpose
signInLogin (email + password + TOTP)
signOutEnd session
enrollTotpBegin TOTP enrollment
verifyTotpComplete enrollment
disableTotpDisable TOTP
regenerateRecoveryCodesNew codes

Calling a server action

From a client component:

"use client";

import { createFinding } from "@/lib/findings/actions";

async function handleSubmit() {
  const result = await createFinding({
    engagementId: "uuid",
    title: "SQL Injection",
    severity: "high",
  });

  if (result.ok) {
    // Success
  } else {
    // result.error has the message
  }
}

Server actions are called like regular functions. TypeScript knows the input and output types.

API routes

Routes live under apps/web/src/app/api/. They use Next.js route handlers.

Auth — /api/auth/[...nextauth]

Standard NextAuth handler. Handles login, logout, session, and CSRF.

Methods: GET, POST

Reports PDF — /api/reports/[id]/pdf

Generates and downloads a report PDF.

Methods: GET

Response: application/pdf

Deliveries — /api/deliveries/[token]/view

Records a view on a delivery link.

Methods: POST

Public — no session required. Used by the delivery page.

AI ping — /api/ai/ping

Checks whether Ollama is reachable.

Methods: GET

Response: { ok: true, model: "llama3.2:3b", host: "http://localhost:11434" }

AI search — /api/ai/search/kb

Semantic search over the KB.

Methods: POST

Body:

{
  "query": "how to exploit delegation",
  "limit": 10
}

Response:

{
  "results": [
    { "slug": "ad-delegation", "similarity": 0.82 },
    ...
  ]
}

Authentication

API routes check the session cookie automatically via middleware. If no valid session exists, the route returns 401.

For scripts that don't have a browser session, options:

  1. Cookie extraction — log in via the browser, copy the session cookie, use it in your HTTP client
  2. Direct server action call — if you have Node.js access, import the action and call it directly with the same session context

There's no API token system. The single-user model assumes you're the operator.

Audit logging

Every mutating server action writes one row to audit_log:

await db.insert(auditLog).values({
  userId: user.id,
  action: "finding.create",
  entity: "finding",
  entityId: finding.id,
  meta: { severity: finding.severity },
});

The audit log is append-only. It's the authoritative record of what happened in the system.

Error handling

Server actions return a structured result:

type ActionResult<T = {}> =
  | ({ ok: true } & T)
  | { ok: false; error: string };

Check .ok before accessing fields. On error, .error has a user-facing message.

API routes return standard HTTP errors:

StatusMeaning
200Success
400Validation failed
401No valid session
403Session valid but not authorized
404Not found
429Rate limited
500Server error

Rate limiting

Only /api/auth/check-password is rate-limited (5 attempts per 15 minutes per IP+email). Other endpoints rely on the single-user model and VPN-only access.

Webhooks

Outbound webhooks fire on events (see Event bus). Payload format and signature verification are covered in Notifications.

Using the API from scripts

Example — fetch all critical findings:

import { db, findings } from "@soc/db";
import { eq } from "drizzle-orm";

const critical = await db
  .select()
  .from(findings)
  .where(eq(findings.severity, "critical"));

Direct database access is often simpler than HTTP. The Drizzle schema is importable from @soc/db.

Extending the API

To add a new API route:

  1. Create apps/web/src/app/api/<path>/route.ts
  2. Export GET, POST, etc.
  3. Use auth() to check the session
  4. Validate input with Zod
  5. Return a Response or JSON

Example:

import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";

export async function GET() {
  const session = await auth();
  if (!session) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  return NextResponse.json({ ok: true });
}

Where to go next