# apps/admin_api — Private Admin API

The inward-facing API that powers Arda and the factory floor. Moderation actions, channel / media management, the entire Beyond order pipeline, inventory and shipping, worker threads for CSV exports and scan processing — all of it lives here. This service runs behind an EC2 security group that only admits a curated list of IP addresses (Bigscreen office + factory network). If it's not reachable from the public internet, that's by design.

**Entry:** [apps/admin_api/admin_api.ts](../../apps/admin_api/admin_api.ts):26 — port defaults to 3999 (`argv[2]` can override).
**Package:** `bigscreen_admin_api` (yarn workspace).
**Depends on:** [`@bigscreen/lib`](../libraries/lib.md), [`@bigscreen/auth`](../libraries/auth.md), [`@bigscreen/api`](../libraries/api-handlers.md), plus the KB service.

## Auth Handler Fan-Out

Unlike `apps/api`, which has a single auth handler, `admin_api` hosts **four** distinct API identities on the same Express app, one per client category. `/admin/*` additionally has a **second admission path** for OAuth JWT bearers.

```mermaid
flowchart TB
    REQ[HTTP request]
    ADMIN["/admin/*<br/>verifyAdminRequest"]
    FUSION["/fusion/*<br/>fusionAuthHandler"]
    CNC["/cnc/*<br/>cncMachineAuthHandler"]
    POSE["/pose/*<br/>poseAuthHandler"]
    HEALTH["/health<br/>(no auth)"]

    REQ --> ADMIN
    REQ --> FUSION
    REQ --> CNC
    REQ --> POSE
    REQ --> HEALTH

    ADMIN --> SHAPE{bearer shape?}
    SHAPE -- "64-char app key" --> LEGACY["adminAuthHandler<br/>(admin-api key +<br/>access token +<br/>access policy)"]
    SHAPE -- "xxx.yyy.zzz JWT" --> OBEARER["handleOAuthBearer<br/>(OAuth access token +<br/>active grant + scope)"]
    LEGACY --> DOMAIN1[Admin domain handlers]
    OBEARER --> DOMAIN1

    FUSION -->|"fusion-api key<br/>(LB-protected,<br/>no access token)"| DOMAIN2[Fusion 360 job handlers]
    CNC -->|cnc-machine-api key +<br/>access token| DOMAIN3[CNC toolpath pull]
    POSE -->|pose-api key +<br/>access token| DOMAIN4[Face pose analysis]
```

Created at [admin_api.ts:49-216](../../apps/admin_api/admin_api.ts):

```ts
const adminAuthHandler       = Auth.AuthHandler.createAuthHandler({ apiName: "admin-api" });
const fusionAuthHandler      = Auth.AuthHandler.createAuthHandler({ apiName: "fusion-api" });
const cncMachineAuthHandler  = Auth.AuthHandler.createAuthHandler({ apiName: "cnc-machine-api" });
const poseAuthHandler        = Auth.AuthHandler.createAuthHandler({ apiName: "pose-api" });
```

Fusion machines deliberately don't need an access token — they're identified by being inside the factory network (load-balancer gated). CNC and pose tools do need a login because they're operated by humans.

The OAuth JWT path in `verifyAdminRequest` ([admin_api.ts:128-164](../../apps/admin_api/admin_api.ts)) inspects the bearer shape — a three-segment `xxx.yyy.zzz` of base64url chars is routed to `handleOAuthBearer`, which verifies the access-token signature, checks the token-hash is still in the active Redis set, and loads the `oauth_clients` + `oauth_grants` rows to reject disabled clients / revoked grants / scope mismatches. On success it populates `req.verifiedAccessToken`, `req.currentAccount`, and `req.authorizedApp = oauth:<clientId>`, so downstream `AuthApi.requireScopeAndPolicy` enforcement is identical to the legacy path. Gated on `OAUTH_ADMIN_API_ENABLED=true`; when disabled, JWT bearers are rejected and only app-key bearers work. See [services/oauth.md](./oauth.md) for the full diagram.

## Endpoint Categories

Admin routes are registered directly in [admin_api.ts](../../apps/admin_api/admin_api.ts) (one massive file). The groups below are the main path prefixes.

| Prefix | Purpose | Handler origin |
|--------|---------|----------------|
| `/admin/account/*` | Staff/onboarding accounts, access policies, login tokens, device history | `@bigscreen/api` (`Admin`) |
| `/admin/account/:id/*` | Ban / disable / report / analytics / notes on a specific account | `@bigscreen/api` (`Moderator`) |
| `/admin/account/:id/social/*` | Social profile + friend graph overrides | `@bigscreen/api` (`Moderator`) |
| `/admin/tv/*` | TV channels + channel groups CRUD, texture rebuilds | `@bigscreen/api` (`Channels`) |
| `/admin/media/*` | Media item CRUD, cloning, CDN queries | `@bigscreen/api` (`Media`) |
| `/admin/shop/*`, `/admin/order/*`, `/admin/scan/*` | Beyond orders, Shopify pairing, RDC builds, face-scan toolpaths | `FabricatorAdminApi` |
| `/admin/inventory/*` | Inventory operations | `InventoryAdminApi` |
| `/admin/shipping/*` | Shipping label generation, fulfilment, pickup (see [shipping-configurations.md](./shipping-configurations.md) for the full shipment-group / box-size map) | `ShippingAdminApi` |
| `/admin/dhl/*` | DHL invoice upload + reconciliation | `DHLInvoiceApi` |
| `/admin/workers/*` | Worker-thread status / lifecycle | In-process (`WORKER_TASKS`) |
| `/admin/kb/*` | Knowledge-base admin (stats, docs, feedback) | `apps/kb` (via `KBApi`) |
| `/admin/oauth/*` | OAuth 2.0 client CRUD, user grants, audit log, step-up verification. Used by arda's consent UI and by SuperUser client administration. Full list in [services/oauth.md](./oauth.md). | `@bigscreen/api` (`OAuthClientsApi`) |
| `/fusion/*` | Fusion 360 job endpoints | `FabricatorAdminApi` |
| `/cnc/*` | CNC toolpath pull | `FabricatorAdminApi` |
| `/pose/*` | Face-pose analysis tool endpoints | `FabricatorAdminApi` |
| `/health` | Plain "OK" probe | Inline |

## Access Policies

Most `/admin/*` routes additionally gate on an access-policy claim carried in the admin's access token. The full enum lives in [auth/Schemas.ts](../../auth/Schemas.ts), but the roles most commonly checked here are:

- `Admin`, `SuperUser` — full access (SuperUser > Admin)
- `Moderator` — account moderation, report handling
- `CommunityModerator` — community-specific moderation
- `AccountsReadOnly` — read account data without mutation rights
- `Fabricator`, `FabricatorAdmin`, `FabricatorReadOnly` — Beyond order pipeline
- `Inventory` — inventory operations
- `Reporting` — reporting / analytics access
- `KBAdmin` — knowledge-base management

Route registration uses `AuthApi.getAccessPolicyHandler([AuthSchemas.AccessPolicy.Fabricator, ...])` (legacy, policy-only) or `AuthApi.requireScopeAndPolicy({ scopes, policies })` (current, scope **and** policy) as an additional middleware after the auth handler. The `requireScopeAndPolicy` variant is the one the OAuth provider depends on — it enforces that an OAuth bearer carries at least one required scope AND that the underlying user still holds at least one required policy. Legacy app-key + access-token requests pass the scope check automatically (no scope = unrestricted, as before).

## Worker Threads

Long-running jobs (Big Order syncing, shipper queue refresh, Shippo analytics, KB ingest) don't block the Express event loop. `admin_api.ts` has two spawn helpers: `startTaskWorker(taskName, req)` (uses the `WORKER_TASKS` registry in [worker_tasks.ts](../../apps/admin_api/worker_tasks.ts) + generic [task_runner.ts](../../apps/admin_api/task_runner.ts)) and `startWorkerProcess(file, req)` (custom-lifecycle workers like `kb_sync_worker`).

```mermaid
sequenceDiagram
    autonumber
    actor ARDA as Arda operator
    participant ADMIN as apps/admin_api
    participant REG as activeWorkers map
    participant WT as Worker thread
    participant DISC as Discord DiscordLogger

    ARDA->>ADMIN: POST /admin/shop/worker/<task>
    ADMIN->>WT: new Worker(task_runner.js, {taskName, request})
    ADMIN->>REG: activeWorkers.set(threadId, WorkerInfo)
    ADMIN-->>ARDA: 202 { jobId }

    WT->>WT: runs task (DB / Redis / Shopify / Shippo etc.)
    WT-->>ADMIN: message (completed) or error

    ADMIN->>DISC: "Worker X finished after …"
    ADMIN->>REG: mark info.status = completed
    Note over REG: auto-expires after 60s so<br/>UI can still see "recently finished"
```

An operator watching the worker panel in Arda sees live status, duration, and who kicked the job off via `GET /admin/workers/status`. The tracking logic lives in `setupWorkerTracking()` at [admin_api.ts:157](../../apps/admin_api/admin_api.ts).

**For the full list of registered workers, the data each one mutates, the Redis keys they own, and how to add a new one**, see the dedicated page: [admin-api-workers.md](./admin-api-workers.md).

## Uploads

Two multer-backed upload pipelines live on the service:

- **Scan uploads** — `UploadHandler("scan", __dirname + "/.scans", "tmp-scan")` — used by face-scan submission flows.
- **Tool-path uploads** — `UploadHandler("tool-path", __dirname + "/.tool-paths", "tmp-tool-path")` — used when Fusion 360 posts back a finished toolpath.

There's also an in-memory CSV uploader (`csvUpload`) used for DHL invoice reconciliation.

## Startup Notes

- `FabricatorAdminApi.initialize()` runs at boot ([admin_api.ts:34](../../apps/admin_api/admin_api.ts)) — this primes caches and DB clients for the order pipeline.
- Body-parser limit is bumped to `20mb` ([admin_api.ts:29](../../apps/admin_api/admin_api.ts)) to accept larger admin payloads (batch order imports, etc).
- KB endpoints are wired in by `import * as KBApi from "../kb/src/api/KBApi"` — physically separate workspace but same process.

## Per-endpoint reference

**For the exact method, path, auth, request, response, and error codes of every endpoint**, see the per-module reference directory: **[admin-api/README.md](./admin-api/README.md)**.

The files there are the canonical HTTP reference for internal teams — Arda devs, factory tooling, integration tests — who only have the docs and not the code.

Highlights:

- [admin-api/authentication.md](./admin-api/authentication.md) — the four API identities, bearer formats, access policies, scopes
- [admin-api/accounts.md](./admin-api/accounts.md) / [admin-api/moderation.md](./admin-api/moderation.md) — staff accounts, banning, reports
- [admin-api/media.md](./admin-api/media.md) / [admin-api/channels.md](./admin-api/channels.md) — media items, products, entitlements, TV channels
- [admin-api/fabricator-orders.md](./admin-api/fabricator-orders.md) / [admin-api/fabricator-jobs.md](./admin-api/fabricator-jobs.md) / [admin-api/fabricator-scans.md](./admin-api/fabricator-scans.md) — Beyond order pipeline, jobs, face-scan requests
- [admin-api/inventory.md](./admin-api/inventory.md) / [admin-api/shipping.md](./admin-api/shipping.md) / [admin-api/dhl.md](./admin-api/dhl.md) — inventory, shipping, DHL invoices
- [admin-api/oauth.md](./admin-api/oauth.md) — OAuth client CRUD (pairs with [oauth.md](./oauth.md) for protocol flow)
- [admin-api/knowledge-base.md](./admin-api/knowledge-base.md) / [admin-api/workers.md](./admin-api/workers.md) — KB admin, worker-thread lifecycle
- [admin-api/cnc.md](./admin-api/cnc.md) / [admin-api/fusion.md](./admin-api/fusion.md) / [admin-api/pose.md](./admin-api/pose.md) — factory-floor machine endpoints
- [admin-api/\_test-coverage.md](./admin-api/_test-coverage.md) — which integration tests cover which endpoints (generated)

## Further reading

- Full worker-task inventory + what each one mutates → [admin-api-workers.md](./admin-api-workers.md)
- What each Fabricator / Shipping / Inventory module does → [libraries/api-handlers.md](../libraries/api-handlers.md)
- Access-policy plumbing → [libraries/auth.md](../libraries/auth.md)
- OAuth 2.0 provider — client CRUD, grants, audit, bearer admission → [services/oauth.md](./oauth.md)
- Arda's proxy model (Arda doesn't re-implement auth — it forwards) → [webapps/arda.md](../webapps/arda.md)
