# API Reference

Complete API documentation for Stratofusion's backend endpoints.

The combined Google, OneDrive, and Dropbox listing/OAuth endpoints can return
HTTP 200 with `{ "url": "..." }` when provider credentials are missing.
For GET folder reads (`folderId` supplied), the shared `apiGet` client translates
that response into `AUTHENTICATION_REQUIRED`, retaining the requested service
and account identity. The drive view then requests reconnection instead of
interpreting the response as an empty listing. Explicit add-account and OAuth
callback requests retain their URL response; a valid `{ "items": [] }` listing
still represents an empty folder. Saved connection metadata alone does not
establish that provider credentials are available.

OAuth account-type mismatches retain the selected and detected account types
in dialogs and toast feedback, with a **Choose Account Type** action. The
generic authentication formatter must not replace this explanation with a
disconnect/reconnect instruction. Dropbox's `account_type` API field determines
the detected type; a Google sign-in method or personal email does not establish
that the Dropbox account is classified as personal. Personal and Work/School
Dropbox connections currently request the same user-level OAuth scopes.

Running backup/sync job responses expose optional
`operationSummary.current.progressView` with `label`, `percentage` (nullable),
`indeterminate`, `listedCount`, `checkedCount`, and `countersText`.
The server derives this provider-neutral view from the worker snapshot.
Unknown scan totals produce a null percentage; counts describe observed
listing/checking work and can cover both endpoints. Existing summary fields
remain available for older clients.

---

## Table of Contents

1. [Overview](#overview)
2. [Authentication](#authentication)
3. [Response Format](#response-format)
4. [File Operations](#file-operations)
5. [Hierarchy Operations](#hierarchy-operations)
6. [Search Operations](#search-operations)
7. [Backup Operations](#backup-operations)
8. [Service Management](#service-management)
9. [Admin Operations](#admin-operations)
10. [Error Handling](#error-handling)

---

## Overview

### Base URL

- **Development:** `http://localhost:3000/api`.
- **Production:** `https://stratofusion.io/api` on the authoritative OVHcloud VM.
- **Legacy:** a retained Vercel production deployment exists for approved,
  database-reconciled recovery only; it is not the normal API base URL.

### API Conventions

- All endpoints use **REST** principles.
- All requests/responses use **JSON** format.
- All endpoints require **authentication** unless explicitly documented as public, such as webhooks, OAuth status/callback helpers, policy docs, signed event streams, or the allowlisted article-engagement endpoint.
- All responses follow the **standardized ApiResponse format**.
- Debug and dev endpoints require server-side `dev`-or-higher role checks; frontend-only checks are not sufficient.
- Download-session and delete endpoints reject unauthenticated callers before provider-token lookup or destructive work.
- Browser requests using `POST`, `PUT`, `PATCH`, or `DELETE` must carry exact
  same-origin `Origin` or Fetch Metadata evidence. Rejected request-integrity
  checks return `403` with `Cache-Control: no-store` before authentication or
  handler execution.
- Behind Caddy, exact-origin comparison uses the public request `Host`
  (including its port) and the request URL scheme, rather than the standalone
  Next.js listening address. Sibling hosts, different schemes/ports, and
  cross-site requests remain rejected; no sync-specific exemption is used.
- Credentialless webhook, cron, and internal-service calls remain supported and
  must pass the endpoint's signature, bearer, or service authentication.

### Public article engagement

```http
POST /api/analytics/articles
Content-Type: text/plain;charset=UTF-8
```

This best-effort public endpoint records aggregate editorial engagement. It
accepts a JSON body up to 2 KiB and returns `204` for a supported event. The
allowlist covers article views, audience-filter selection, library/homepage/
related-article clicks, CTA clicks, and article-attributed signup start or
completion.

Only fixed event/source/destination values, a supported audience, and safe
article slugs are retained. Unknown fields are discarded, invalid combinations
return `400`, oversized payloads return `413`, and browser requests explicitly
marked cross-site return `403`. The handler intentionally does not add IP
addresses, user agents, referrers, query strings, account identifiers, or email
addresses to its structured `ARTICLE_ENGAGEMENT` application logs.

---

## Authentication

### OAuth Flow

#### 1. Initiate OAuth

```http
GET /api/{service}?accountId={accountId}&popup=true
```

**Parameters:**

- `service` - Service type (`google`, `onedrive`, `dropbox`).
- `accountId` - Account identifier (optional, defaults to `default`).

**Response:**

- Returns the provider authorization URL in the standard response envelope.
- Returns `403` when the request host is not a registered stable OAuth host or
  the exact configured Phase-B rehearsal origin.
- The direct request authority and `X-Forwarded-Host`, when both are present,
  must agree after default-port normalization. A forwarded header cannot
  authorize an otherwise unsupported request host.

#### 2. OAuth Callback

```http
GET /api/{service}?code={code}&state={state}
```

**Parameters:**

- `code` - Authorization code from service.
- `state` - Opaque StratoFusion flow context returned unchanged by the provider.

**Response:**

- Redirects to the validated public application origin with success/error.
- Returns `400` before token exchange when `state` is missing, malformed,
  expired, legacy unsigned, or fails its HMAC signature. State is valid for ten
  minutes and includes a random nonce; callers must treat it as opaque.
- Initiation returns `401` if no authenticated Clerk user is available to bind
  the provider flow.
- Callback redirects use the validated `NEXT_PUBLIC_SITE_URL`; raw forwarded
  host, port, and protocol values are never emitted as redirect targets.
- Google Drive, OneDrive, and Dropbox are the implemented direct OAuth service
  values. Scaffolded future providers are not accepted here.
- `GET` and `POST /api/auth/clerk-oauth` enforce the same incoming-host policy
  before reading Clerk accounts or attempting provider auto-connection.

#### 3. Check Auth Status

```http
GET /api/auth/status?service={service}&accountId={accountId}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "authenticated": true,
    "service": "google",
    "accountId": "default",
    "email": "user@example.com"
  }
}
```

#### 4. Disconnect Service

```http
POST /api/auth/disconnect
Content-Type: application/json

{
  "service": "google",
  "accountId": "default"
}
```

#### 5. Logout Token Cleanup

```http
POST /api/auth/logout?service={optional-supported-service}
```

Requires an authenticated Clerk session. Deletes encrypted provider-token rows
for the requested supported service, or all provider-token rows when omitted,
while preserving connected-account metadata and user preferences. Unsupported
service values return `400`; explicit disconnect owns account removal.

---

## Response Format

Stratofusion is transitioning to a standardized response format managed by the `src/shared/api` module. This aligns with the Ports & Adapters architecture by ensuring consistent communication across different providers.

### Standard Success Response

Newer endpoints return the data directly as a JSON object, while legacy endpoints may still use the `success: true` wrapper.

```json
{
  "id": "file123",
  "name": "document.pdf",
  "size": 1024000
}
```

### Standard Error Response

Errors are normalized into a consistent structure across all services via `createErrorResponse`.

```json
{
  "error": "File not found",
  "code": "NOT_FOUND",
  "provider": "google",
  "retryable": false
}
```

### Error Codes (`src/shared/errors/`)

The application defines a set of business-level error codes that are mapped from provider-specific errors:

| Code                | Description              | HTTP Status |
| ------------------- | ------------------------ | ----------- |
| `AUTH_FAILED`       | Authentication failed    | 401         |
| `PERMISSION_DENIED` | Insufficient permissions | 403         |
| `NOT_FOUND`         | Resource not found       | 404         |
| `RATE_LIMITED`      | Too many requests        | 429         |
| `QUOTA_EXCEEDED`    | Storage quota reached    | 429         |
| `INVALID_REQUEST`   | Invalid parameters       | 400         |
| `NETWORK_ERROR`     | Connection issues        | 503         |

---

---

## File Operations

### List Files

```http
GET /api/{service}/files?folderId={folderId}&accountId={accountId}
```

**Parameters:**

- `service` - Service type (`google`, `onedrive`, `dropbox`).
- `folderId` - Folder ID (use `root` for root folder).
- `accountId` - Account identifier (optional, defaults to `default`).

**Response:**

```json
{
  "success": true,
  "data": {
    "files": [
      {
        "id": "file123",
        "name": "document.pdf",
        "mimeType": "application/pdf",
        "size": 1024000,
        "modifiedTime": "2025-10-15T10:00:00Z",
        "isFolder": false,
        "parentId": "folder456"
      }
    ],
    "nextPageToken": null
  }
}
```

### Get File Metadata

```http
GET /api/{service}/files/{fileId}?accountId={accountId}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "id": "file123",
    "name": "document.pdf",
    "mimeType": "application/pdf",
    "size": 1024000,
    "modifiedTime": "2025-10-15T10:00:00Z",
    "isFolder": false,
    "parentId": "folder456",
    "webViewLink": "https://...",
    "downloadUrl": "https://..."
  }
}
```

### Upload File

```http
POST /api/{service}/upload
Content-Type: multipart/form-data

{
  "file": <file>,
  "folderId": "folder456",
  "accountId": "default"
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "id": "file789",
    "name": "uploaded.pdf",
    "size": 2048000
  }
}
```

### Delete Files Or Folders

```http
POST /api/delete
Content-Type: application/json

{
  "items": [
    {
      "service": "onedrive",
      "accountId": "account-123",
      "itemId": "item-456"
    }
  ],
  "useTrash": false
}
```

**Response:**

```json
{
  "success": true,
  "results": [
    {
      "id": "item-456",
      "service": "onedrive",
      "accountId": "account-123",
      "success": true,
      "disposition": "trash",
      "trashedOnly": true
    }
  ]
}
```

`disposition` is the operation the provider actually completed: `trash` or
`permanent`. It is not copied from `useTrash`. For example, if permanent delete
is unavailable and the provider successfully falls back to its recycle bin,
the result is `disposition: "trash"` and `trashedOnly: true`.

`POST /api/delete/stream` accepts the same request and returns NDJSON progress.
Each `item_complete.result` and the final response use the same actual-outcome
contract.

### Rename File

```http
PATCH /api/{service}/files/{fileId}
Content-Type: application/json

{
  "name": "new-name.pdf",
  "accountId": "default"
}
```

---

## Hierarchy Operations

These endpoints provide information about the organizational structure of cloud providers (Shared Drives, Sites, Team Folders).

### Get Account Hierarchy

Unified endpoint to get top-level organizational containers for a service account.

```http
GET /api/hierarchy?service={service}&accountId={accountId}
```

**Parameters:**

- `service` (required) - Service type (`google`, `onedrive`, `dropbox`).
- `accountId` (required) - The account ID to get hierarchy for.

**Response:**

```json
{
  "hierarchy": [
    {
      "id": "drive123",
      "name": "Project Alpha",
      "type": "shared_drive"
    }
  ],
  "serviceType": "google"
}
```

### Get Folder Hierarchy (Breadcrumbs)

Unified endpoint to fetch the folder hierarchy (breadcrumb path) for any folder ID.

```http
GET /api/folders/hierarchy?service={service}&folderId={folderId}&accountId={accountId}
```

**Parameters:**

- `service` (required) - Service type (`google`, `onedrive`, `dropbox`).
- `folderId` (required) - The folder ID / path.
- `accountId` (required) - The account ID.

**Response:**

```json
{
  "breadcrumbs": [
    {
      "id": "folder123",
      "name": "Documents",
      "service": "google",
      "accountId": "acc_123"
    }
  ]
}
```

---

## Search Operations

### Basic Search

```http
POST /api/search/basic
Content-Type: application/json

{
  "query": "resume",
  "services": ["google", "onedrive"],
  "accountIds": {
    "google": "default",
    "onedrive": "default"
  }
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "results": [
      {
        "service": "google",
        "accountId": "default",
        "file": {
          "id": "file123",
          "name": "resume.pdf",
          "size": 512000,
          "modifiedTime": "2025-10-15T10:00:00Z"
        }
      }
    ],
    "totalResults": 1
  }
}
```

### Full-Text Search

```http
POST /api/search/fulltext
Content-Type: application/json

{
  "query": "annual report 2024",
  "services": ["google"],
  "accountIds": {
    "google": "default"
  }
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "results": [
      {
        "service": "google",
        "file": {
          "id": "file123",
          "name": "report.docx",
          "snippet": "...annual report 2024..."
        }
      }
    ]
  }
}
```

---

## Job Management Operations

> **Note:** The job management system supports both **backup jobs** and **sync jobs** through a unified service layer architecture. All endpoints automatically detect and handle both job types.

### Architecture Overview

```
API Routes (HTTP Layer)
    ↓
Job Service (Business Logic)
    ↓
Job Mappers (Data Transformation)
    ↓
Database Functions
```

**Key Features:**

- Automatic job type detection.
- Unified error handling.
- Type-safe interfaces.
- Consistent response formats.

---

### Create Backup Job

```http
POST /api/jobs/backup
Content-Type: application/json

{
  "sourceService": "google",
  "sourceAccountId": "default",
  "sourceItems": [
    {
      "id": "file123",
      "name": "document.pdf",
      "isFolder": false
    }
  ],
  "destinationService": "onedrive",
  "destinationAccountId": "default",
  "destinationFolderId": "folder456",
  "schedule": "daily",
  "scheduledTime": "14:30",
  "timezone": "America/New_York"
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "jobId": "job_abc123",
    "status": "scheduled",
    "nextRunAt": "2025-10-16T14:30:00Z"
  }
}
```

### Create Sync Job

```http
POST /api/jobs/sync
Content-Type: application/json

{
  "mode": "two-way",
  "sources": [
    {
      "service": "google",
      "accountId": "default",
      "folderId": "folder123",
      "folderName": "Documents"
    }
  ],
  "destination": {
    "service": "dropbox",
    "accountId": "default",
    "folderId": "root",
    "folderName": "Dropbox"
  },
  "schedule": "daily",
  "scheduledTime": "08:00",
  "timezone": "Australia/Sydney",
  "maintainStructure": true,
  "applyFilters": true
}
```

**Parameters:**

- `mode` - Sync mode: `"one-way"` (source → destination) or `"two-way"` (bidirectional).
- `sources` - Array of source locations.
- `destination` - Destination location.
- `schedule` - Schedule type: `"none"`, `"daily"`, `"weekly"`, `"monthly"`, `"hourly-N"`, or `"minutely-N"`.
- `scheduledTime` - Time in HH:mm format (24-hour).
- `timezone` - IANA timezone identifier.
- `maintainStructure` - Whether to preserve folder structure (default: true).
- `applyFilters` - Whether to apply transfer filters (default: true).

**Behavior:**

- The endpoint creates the job record and returns immediately. It does not recursively enumerate folders during job creation.
- Quota and file-size validation happens during the launch phase before the rclone operation starts.
- Large one-way syncs use a 30-minute meaningful-progress stall window instead of a fixed 12-hour runtime cap. The window also covers silent startup; repeating speed/current-file text alone does not reset it. Two-way sync runtime budgets are unchanged.

**Response:**

```json
{
  "success": true,
  "data": {
    "jobId": "sync_xyz789",
    "status": "scheduled",
    "mode": "two-way",
    "nextRunAt": "2025-10-31T08:00:00+11:00"
  }
}
```

### List Jobs

```http
GET /api/jobs?status={status}&limit={limit}
```

**Parameters:**

- `status` - Filter by status (optional): `"scheduled"`, `"running"`, `"completed"`, `"failed"`, `"cancelled"`.
- `limit` - Max results (default: 50).

**Response:**

```json
{
  "success": true,
  "data": {
    "jobs": [
      {
        "id": "job_abc123",
        "jobType": "backup",
        "status": "scheduled",
        "schedule": "daily",
        "scheduledTime": "14:30",
        "timezone": "America/New_York",
        "sourceSummary": "Google Drive: /Documents",
        "destinationSummary": "OneDrive: /Backups",
        "nextRunAt": "2025-10-16T14:30:00Z",
        "createdAt": "2025-10-15T10:00:00Z",
        "updatedAt": "2025-10-15T10:00:00Z",
        "mode": null
      },
      {
        "id": "sync_xyz789",
        "jobType": "sync",
        "status": "scheduled",
        "schedule": "daily",
        "scheduledTime": "08:00",
        "timezone": "Australia/Sydney",
        "sourceSummary": "text-and-code (dropbox)",
        "destinationSummary": "google (google_stratofusion002_1761708536832_5450)",
        "nextRunAt": "2025-10-31T08:00:00+11:00",
        "createdAt": "2025-10-31T07:48:00+11:00",
        "updatedAt": "2025-10-31T07:48:00+11:00",
        "mode": "two-way"
      }
    ]
  }
}
```

**Response Fields:**

- `jobType` - Type of job: `"backup"` or `"sync"`.
- `status` - Job status: `"scheduled"`, `"running"`, `"completed"`, `"failed"`, `"cancelled"`.
- `schedule` - Schedule type: `"none"`, `"daily"`, `"weekly"`, `"monthly"`, `"hourly-N"`, or `"minutely-N"`.
- `mode` - Sync mode (sync jobs only): `"one-way"` or `"two-way"`, `null` for backup jobs.

### Get Job Details

```http
GET /api/jobs/{jobId}
```

**Job Type Detection:**

- Tries `backup_jobs` table first.
- Falls back to `sync_jobs` table if not found.
- Returns 404 if job not found in either table.

**Response for Backup Job:**

```json
{
  "success": true,
  "data": {
    "id": "job_abc123",
    "status": "scheduled",
    "schedule": "daily",
    "scheduledTime": "14:30",
    "timezone": "America/New_York",
    "sourceSummary": "Google Drive: /Documents",
    "destinationSummary": "OneDrive: /Backups",
    "createdAt": "2025-10-15T10:00:00Z",
    "updatedAt": "2025-10-15T10:00:00Z",
    "nextRunAt": "2025-10-16T14:30:00Z",
    "sources": [...],
    "destination": {...}
  }
}
```

**Response for Sync Job:**

```json
{
  "success": true,
  "data": {
    "id": "sync_xyz789",
    "status": "scheduled",
    "mode": "two-way",
    "resyncBehavior": "auto",
    "conflictStrategy": "newer-wins",
    "bisyncInitialized": false,
    "schedule": "daily",
    "scheduledTime": "08:00",
    "timezone": "Australia/Sydney",
    "sourceSummary": "text-and-code (dropbox)",
    "destinationSummary": "google (google_stratofusion002_1761708536832_5450)",
    "createdAt": "2025-10-31T07:48:00+11:00",
    "updatedAt": "2025-10-31T07:48:00+11:00",
    "nextRunAt": "2025-10-31T08:00:00+11:00",
    "sources": [...],
    "destination": {...},
    "maintainStructure": true,
    "applyFilters": true,
    "verifyWithChecksum": false
  }
}
```

### Update Job

```http
PATCH /api/jobs/{jobId}
Content-Type: application/json
```

**Job Type Detection:**

- Detects job type by presence of `mode` field in request body.
- If `mode` field present → sync job.
- If `mode` field absent → backup job.

**Request for Backup Job:**

```json
{
  "schedule": "weekly",
  "scheduledTime": "18:00",
  "timezone": "America/New_York",
  "sources": [...],
  "destination": {...}
}
```

**Request for Sync Job:**

```json
{
  "mode": "one-way",
  "resyncBehavior": "auto",
  "conflictStrategy": "newer-wins",
  "schedule": "daily",
  "scheduledTime": "08:00",
  "timezone": "Australia/Sydney",
  "sources": [...],
  "destination": {...},
  "maintainStructure": true,
  "applyFilters": true,
  "verifyWithChecksum": false
}
```

**Response:**

```json
{
  "success": true,
  "data": {
    "id": "job_abc123",
    "status": "scheduled",
    "nextRunAt": "2025-10-17T18:00:00Z"
  }
}
```

### Cancel Job

```http
POST /api/jobs/{jobId}/cancel
```

**Job Type Detection:**

- Tries `backup_jobs` table first.
- Falls back to `sync_jobs` table if not found.
- Returns 404 if job not found in either table.

**Response:**

```json
{
  "success": true,
  "data": {
    "id": "job_abc123",
    "status": "cancelled"
  }
}
```

### Delete Job

```http
DELETE /api/jobs/{jobId}
```

**Job Type Detection:**

- Tries `backup_jobs` table first.
- Falls back to `sync_jobs` table if not found.
- Returns 404 if job not found in either table.

**Response:**

```json
{
  "success": true,
  "data": {
    "deleted": true
  }
}
```

---

## Service Management

### List Connected Services

```http
GET /api/services/connected
```

**Response:**

```json
{
  "success": true,
  "data": {
    "services": [
      {
        "service": "google",
        "accountId": "default",
        "email": "user@gmail.com",
        "connected": true
      }
    ]
  }
}
```

### Get Storage Quota

```http
GET /api/storage/quota?service={service}&accountId={accountId}
```

Supported services are `google`, `onedrive`, and `dropbox`. Dropbox quota reads first verify that the token-bound Dropbox email matches the saved account and that the account is still usable for its stored account type. If credentials belong to another Dropbox identity, or a stored Dropbox Business account is disabled, cancelled, downgraded, or otherwise no longer accessible, the endpoint returns a normalized `accountHealth` payload instead of quota data or generic failure text. Successful Dropbox responses normalize the provider's tagged individual/team allocation union so member-specific usage is never combined with team-wide capacity (or vice versa).

```json
{
  "success": false,
  "error": "Dropbox Business access is no longer available for this account. Reconnect a Dropbox Business account or remove the old account.",
  "errorCode": "ACCESS_DENIED",
  "accountHealth": {
    "service": "dropbox",
    "status": "plan-changed",
    "reason": "business-access-lost",
    "needsReauth": false,
    "expectedAccountType": "business",
    "providerAccountType": "personal"
  }
}
```

A token-bound identity mismatch returns `401` with
`accountHealth.reason = "account-mismatch"`, `needsReauth = true`, and the
normalized saved/provider emails so the user can reconnect the intended
Dropbox account.

### Get Service Capabilities

```http
GET /api/services/{service}/capabilities
```

**Response:**

```json
{
  "success": true,
  "data": {
    "service": "google",
    "capabilities": {
      "search": true,
      "fullTextSearch": true,
      "upload": true,
      "download": true,
      "maxFileSize": 5368709120,
      "maxBatchSize": 53687091200
    }
  }
}
```

### Enhanced Search

```http
GET /api/search/enhanced?q=project%20plan&mode=ai&services=google&accountIds=google-account-1
```

`POST /api/search/enhanced` is also supported for clients that prefer a JSON
body. It normalizes into the same handler as `GET`.

```http
POST /api/search/enhanced
Content-Type: application/json

{
  "q": "project plan",
  "mode": "ai",
  "maxResults": 25,
  "services": ["google"],
  "accountIds": ["google-account-1"],
  "folderId": "folder-1"
}
```

Supported modes are `basic`, `fulltext`, and `ai`. AI mode requires the user's
subscription entitlement plus runtime semantic-search configuration. If the AI
runtime kill switch or semantic infrastructure is disabled, the endpoint returns
`503` with `AI search is disabled by runtime policy` before consuming search
quota.

**AI response metadata:**

```json
{
  "success": true,
  "data": {
    "items": [],
    "searchMode": "ai",
    "aiFallbackUsed": false,
    "indexCoverage": "partial"
  }
}
```

---

## AI Indexing

AI indexing endpoints require an authenticated user and only operate on jobs
owned by that user. Current indexing eligibility is limited to the explicit
AI-indexing allow-list: deterministic text-like formats, PDFs with extractable
embedded text, DOCX files with extractable text, XLSX spreadsheets with
extractable visible cell values, PPTX presentations with extractable slide
text and speaker notes, standalone PNG/JPEG images when OCR is enabled, plus
Google Workspace files exported from Google Drive before extraction. Supported
Google Workspace MIME types are `application/vnd.google-apps.document`,
`application/vnd.google-apps.spreadsheet`,
`application/vnd.google-apps.presentation`, and
`application/vnd.google-apps.drawing`. Google Docs export to DOCX, Google
Sheets export to XLSX, Google Slides export to PPTX, and Google Drawings export
to PDF. Successful Workspace indexing records
`extraction_method: "google_workspace_export"` while preserving the original
Workspace MIME type in indexed metadata. OCR is disabled by default and
requires `AI_SEARCH_OCR_ENABLED=true`; without it, PNG/JPEG files are skipped
with an OCR-required reason. PDF indexing does not render pages for OCR in
Phase 6A. Before DOCX, XLSX, or PPTX parser dispatch, OpenXML ZIP metadata is
checked for bounded entry count, entry and aggregate expanded bytes,
compression ratio, encrypted entries, unsupported ZIP64 size markers, and
unsafe entry paths. Rejected packages produce sanitized extraction failures.
DOCX indexing uses deterministic plain-text extraction for `.docx`
extension and OpenXML document MIME inputs, including parameterized MIME values;
it does not support legacy `.doc`, password-protected or encrypted documents,
embedded image extraction, OCR over embedded images, or complex formatting
preservation. XLSX indexing reads visible worksheets
row by row with `--- Sheet: SheetName ---` delimiters and cell values rather
than formula syntax; it does not support legacy `.xls`, hidden sheets, charts,
images, macros, or OCR. Empty XLSX files skip with `extraction_xlsx_no_text`;
corrupt, malformed, timed-out, or password-protected XLSX files use sanitized
`extraction_xlsx_parse_failed` or `extraction_xlsx_encrypted` taxonomy codes.
PPTX indexing reads non-hidden slides in presentation
order, emits `Slide N:` delimiters, and includes speaker notes when the slide
relationship exposes an identifiable notes body placeholder. Empty PPTX files
skip with `extraction_pptx_no_text`, encrypted PPTX files skip with
`extraction_pptx_encrypted`, and corrupt, malformed, or timed-out PPTX parsing
fails with `extraction_pptx_parse_failed`; PPTX failure messages are sanitized.
It does not support legacy `.ppt`, ODP, Keynote, embedded media, animations,
transitions, timings, OCR, master slide/layout template text, or image alt text.
Empty, corrupt, encrypted, image-only/scanned, or oversized PDFs, DOCX files,
XLSX files, PPTX files, and exported Google Workspace content can be skipped or
failed with an item-level reason instead of producing semantic vectors.
Exported Workspace content is not cached; each indexing run exports
fresh bytes and preserves original file metadata in indexed records. Google
Forms, Sites, Maps, Fusion Tables, Jamboard, Apps Script, Shortcuts, third-party Drive app
files, non-PPTX presentations, other Office formats, GIF, TIFF, BMP, WebP,
HEIC, media, archives, cloud OCR providers, OCR caching, and unknown binary
formats remain unsupported for AI indexing. Export timeout, permission, quota,
network/provider, unsupported-type, invalid-provider, and operational OCR
provider failures fail the affected indexing item with sanitized reasons.
Indexing execution performs a semantic-index backend readiness check before
provider discovery; if the configured vector backend cannot bootstrap its
collection, the job is finalized as failed with a sanitized semantic-index
failure reason and no item rows are created.

### Download stream proxy

`GET /api/download/stream?service={google|dropbox}&token={signed-token}` accepts
only a bounded signed-token shape. The configured worker target must be the
fixed local development endpoint or a public pathless HTTPS origin. Upstream
redirects are refused, and connection failures return a generic `502` response
without the upstream URL or raw network error.

### List Recent AI Indexing Jobs

```http
GET /api/search/indexing/jobs/recent?limit=20
```

Returns active and recent AI indexing jobs for the current user. Active jobs are
sorted first so clients can use the response as the REST source of truth before
opening a live stream.

### Stream AI Indexing Job Updates

```http
GET /api/search/indexing/jobs/events
```

Server-Sent Events stream for `/user/ai-indexing`. The stream emits `update`
events with the same job-list shape as the recent-jobs REST endpoint, `error`
events when snapshot loading fails, heartbeat comments, and a `reconnect` event
before the server rotates the connection.

### Get AI Indexing Job Details

```http
GET /api/search/indexing/jobs/{jobId}
```

Returns a stable job status payload, including item-level statuses and failure
reasons when they were recorded.

### Get AI Indexing Job Diagnostics

```http
GET /api/search/indexing/jobs/{jobId}/diagnostics
```

Returns a user-owned job snapshot, progress, ETA when throughput is meaningful,
extraction method counts, taxonomy error groups, retryable/non-retryable counts,
extraction latency percentiles, slow-item samples, and capped failed/skipped
samples. Messages are sanitized and do not include raw provider paths, extracted
text, stack traces, tokens, or raw provider errors.

### Get AI Indexing Health

```http
GET /api/search/indexing/health?service=google&accountId=account-1&scopeType=account
GET /api/search/indexing/health?service=google&accountId=account-1&scopeType=folder&resourceId=folder-1
```

Returns account or folder health metrics for a connected indexing account:
indexed, failed, unsupported/skipped, pending, stale/deleted counts, coverage
percent, success rates by extraction method, OCR confidence summary, recent jobs,
and whether reindexing is recommended because dirty scope state exists.

### Get AI Indexing File Status

```http
GET /api/search/indexing/files/status?service=google&accountId=account-1&resourceId=file-1
```

Returns the latest known indexed-file state and latest job-item state for the
resource identity, including extraction/index status, last indexed timestamp,
sanitized taxonomy code/message, retryability, recommendation, OCR metadata,
chunk count, text length, extraction duration, and reindex recommendation.

### Cancel AI Indexing Job

```http
POST /api/search/indexing/jobs/{jobId}/cancel
```

Allowed for `pending`, `running`, and `retrying` jobs. The route marks the job
`cancelled` and aborts the in-process runner when it is active, which prevents
stale recovery from restarting the job. Successful cancel controls emit an
`ai-indexing-cancel` activity/audit log event with the source job ID.

### Retry AI Indexing Job

```http
POST /api/search/indexing/jobs/{jobId}/retry
```

Allowed for `failed`, `cancelled`, and `partially_completed` jobs. The route
creates a new indexing job from the original account or folder scope and leaves
the original job as history. Successful retry controls emit an
`ai-indexing-retry` activity/audit log event with the source job ID and retry job
ID in metadata.

### Dismiss AI Indexing Job

```http
POST /api/search/indexing/jobs/{jobId}/dismiss
```

Allowed for terminal jobs. The route hides the job and job-item history from the
user-facing recent list with a `dismissed_at` marker; it does not delete job
rows, item rows, indexed-file metadata, or semantic vectors. Successful dismiss
controls emit an `ai-indexing-dismiss` activity/audit log event.

### Dismiss Completed AI Indexing Jobs

```http
POST /api/search/indexing/jobs/dismiss-completed
```

Soft-dismisses all visible `completed` AI indexing jobs for the authenticated
user. The batch action only updates each job row's `dismissed_at` marker, leaves
failed, cancelled, and partially completed jobs visible, and never deletes
job-item history, indexed-file metadata, or semantic vectors. Successful batch
dismiss controls emit one `ai-indexing-dismiss` activity/audit log event with
the dismissed job count in metadata.

---

## Admin Operations

Admin endpoints require a signed-in user whose Clerk ID is configured as an
admin and whose signed Clerk session records a verified second factor. Missing,
malformed, or unregistered second-factor evidence returns `403` with `Admin
multi-factor authentication required`. This resource-level check is independent
of the middleware redirect boundary.

### Get Infrastructure Overview

```http
GET /api/admin/infrastructure/overview
```

Returns a no-store, read-only operational snapshot shaped as a fleet, currently
containing the single configured authoritative runtime node. The response
includes the sample time, overall/node status, release SHA when available,
CPU, memory, root-filesystem, network, uptime, rclone queue/capacity metrics,
observable service health, allowlisted Compose service replica/health status,
the exactly-one production scheduler invariant, backup freshness, sanitized
warnings, and allowlisted management links. Operational fields live under
`nodes[].operations`: `collector`, `composeServices`, `scheduler`, and
`backup`. Backup data contains timestamps and status summaries only.

Each `links[]` entry contains `id`, `serviceId`, `label`, `href`, `kind`, and
`access`. `serviceId` associates the action with an allowlisted service row;
`kind` describes the destination category and `access` is one of
`same-origin`, `public`, or `private`. Same-origin actions use fixed application
routes. Public links are credential-free HTTPS Grafana or GlitchTip origins in
the configured `stratofusion.io` domain family. Private database-tool links
accept only loopback HTTP URLs intended for SSH forwarding or HTTPS `*.ts.net`
origins. URLs containing credentials, query strings, fragments, public IPs, or
other domains are discarded. Clients must not construct or guess management
URLs.

Prometheus resource queries, application/database readiness, rclone health,
and operational telemetry are collected independently. A dependency failure
normally returns `200` with a partial snapshot, explicit unavailable values,
and warnings. Missing metrics and replica counts are `null` and must not be
interpreted as zero. Unauthenticated callers receive `401`; signed-in
non-admin callers and admins without verified MFA receive `403`. Unexpected
overview failures return a sanitized `503` without internal URLs, queries,
upstream bodies, hostnames, IP addresses, or credentials.

This endpoint exposes only allowlisted aggregate service state. It does not
expose container IDs/names, image names, environment variables, mounts, logs,
raw Docker responses, raw backup paths, private Cockpit access, or
infrastructure mutation controls.

### Preview AI Search Reset

```http
GET /api/admin/users/{userId}/ai-search/reset
```

Returns the user-owned AI semantic search state that will be removed, including
AI indexing jobs, job items, indexed-file metadata, dirty scopes, reconciliation
cursors, active local executions, and estimated vector chunks.

### Reset AI Search State

```http
POST /api/admin/users/{userId}/ai-search/reset
```

Deletes all semantic vectors for the target user first, then clears the user's AI
indexing tables in Neon. If vector deletion fails or the vector backend is not
configured, the Neon cleanup is not run.

### Delete User

```http
DELETE /api/admin/users/{userId}
```

Local and dev hard delete cancels user-owned backup/sync jobs, resets AI search
state, performs external provider and Stripe test cleanup, deletes remaining
Neon user data, then deletes the Clerk user last. Production hard delete remains
blocked.

### Change User Subscription Plan

```http
PATCH /api/admin/users/{userId}/tier
Content-Type: application/json

{
  "tier": "pro"
}
```

Changes an existing managed subscription through Stripe. Immediate upgrades
are projected into Clerk and Postgres through the billing synchronization
boundary; downgrades follow Stripe billing-period scheduling. Direct Clerk tier
writes and caller-supplied expiry timestamps are rejected.

### Grant Internal Subscription Entitlement

```http
PATCH /api/admin/users/{userId}/internal-entitlement
Content-Type: application/json

{
  "tier": "unlimited",
  "reason": "Internal devops testing",
  "expiresAt": null
}
```

Creates a separate Pro or Unlimited application entitlement without changing
Stripe billing. `expiresAt` is optional; `null` means no expiry. The route
requires an administrator session with verified MFA and preserves the user's
Stripe metadata.

### Revoke Internal Subscription Entitlement

```http
DELETE /api/admin/users/{userId}/internal-entitlement
```

Removes only the internal entitlement. The user's Stripe-backed subscription
projection remains unchanged.

---

## Error Handling

### Error Codes

| Code                   | Description                     |
| ---------------------- | ------------------------------- |
| `AUTHENTICATION_ERROR` | Authentication failed           |
| `AUTHORIZATION_ERROR`  | Insufficient permissions        |
| `FILE_NOT_FOUND`       | File or folder not found        |
| `INVALID_REQUEST`      | Invalid request parameters      |
| `RATE_LIMIT_EXCEEDED`  | Too many requests               |
| `SERVICE_UNAVAILABLE`  | Service temporarily unavailable |
| `NETWORK_ERROR`        | Network connection failed       |
| `UNKNOWN_ERROR`        | Unexpected error occurred       |

### Error Response Format

```json
{
  "success": false,
  "error": "Human-readable error message",
  "errorCode": "ERROR_CODE",
  "metadata": {
    "timestamp": "2025-10-15T10:30:00Z",
    "requestId": "req_abc123"
  }
}
```

### Retry Logic

- **Rate Limit Errors** - Retry after delay specified in response.
- **Network Errors** - Retry with exponential backoff (max 3 attempts).
- **Service Unavailable** - Retry after 5 seconds (max 3 attempts).
- **Other Errors** - Do not retry automatically.

---

**Last Updated:** 2026-05-07
**API Version:** 1.1 (Ports & Adapters Alignment)
