From 1d6cc2b8c007d9fdede987ad597d0fc6cabd3b71 Mon Sep 17 00:00:00 2001 From: Darius Cepulis Date: Tue, 17 Feb 2026 13:11:22 -0600 Subject: [PATCH] feat(site): add Mux health check action (#542) Co-authored-by: Claude Opus 4.6 --- site/CLAUDE.md | 2 ++ site/src/actions/mux.ts | 34 ++++++++++++++++++++++++++++++++ site/src/middleware/index.ts | 3 +++ site/src/pages/api/health/mux.ts | 14 +++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 site/src/pages/api/health/mux.ts diff --git a/site/CLAUDE.md b/site/CLAUDE.md index caec2978..63beb8d1 100644 --- a/site/CLAUDE.md +++ b/site/CLAUDE.md @@ -649,6 +649,8 @@ The site uses OAuth for authentication and Mux for video management. Required va | Variable | Purpose | | --- | --- | | `MUX_API_URL` | Override Mux API endpoint (defaults to `https://api.mux.com`) | +| `MUX_TOKEN_ID` | Mux API token ID (for server-side health checks) | +| `MUX_TOKEN_SECRET` | Mux API token secret (for server-side health checks) | | `SENTRY_AUTH_TOKEN` | Sentry error tracking auth token | ## Authentication & Mux Integration diff --git a/site/src/actions/mux.ts b/site/src/actions/mux.ts index 413fb4ab..8c5f101b 100644 --- a/site/src/actions/mux.ts +++ b/site/src/actions/mux.ts @@ -22,7 +22,41 @@ function getMuxClient(token: string | undefined) { }); } +function getHealthMuxClient() { + const tokenId = process.env.MUX_TOKEN_ID || import.meta.env.MUX_TOKEN_ID; + const tokenSecret = process.env.MUX_TOKEN_SECRET || import.meta.env.MUX_TOKEN_SECRET; + + if (!tokenId || !tokenSecret) { + throw new ActionError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'MUX_TOKEN_ID and MUX_TOKEN_SECRET must be set', + }); + } + + return new Mux({ + tokenId, + tokenSecret, + }); +} + export const mux = { + /** Unauthenticated health check that verifies the Mux API proxy layer. */ + health: defineAction({ + handler: async () => { + try { + const muxClient = getHealthMuxClient(); + await muxClient.video.assets.list({ limit: 0 }); + return { ok: true }; + } catch (error) { + if (error instanceof ActionError) throw error; + + throw new ActionError({ + code: 'INTERNAL_SERVER_ERROR', + message: error instanceof Error ? error.message : 'Mux health check failed', + }); + } + }, + }), /** * List video assets with pagination * diff --git a/site/src/middleware/index.ts b/site/src/middleware/index.ts index 18acf129..c5a86827 100644 --- a/site/src/middleware/index.ts +++ b/site/src/middleware/index.ts @@ -22,6 +22,9 @@ function isGated(actionName: string | undefined) { // detect UNAUTHORIZED and show a login UI before uploading if (actionName === 'mux.createDirectUpload') return false; + // Health check uses server-side credentials, not user OAuth + if (actionName === 'mux.health') return false; + // I don't love the magic string nature of this pattern but it's what is recommended in the docs return actionName.startsWith('mux'); } diff --git a/site/src/pages/api/health/mux.ts b/site/src/pages/api/health/mux.ts new file mode 100644 index 00000000..6e15f01d --- /dev/null +++ b/site/src/pages/api/health/mux.ts @@ -0,0 +1,14 @@ +import { actions } from 'astro:actions'; +import type { APIRoute } from 'astro'; + +export const prerender = false; + +export const GET: APIRoute = async (context) => { + const { data, error } = await context.callAction(actions.mux.health, {}); + + if (error) { + return Response.json({ ok: false, error: error.message }, { status: 502 }); + } + + return Response.json(data); +};