-
Notifications
You must be signed in to change notification settings - Fork 0
New SQS For Stripe Links #706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
b4f3064
7a38e12
88eef1a
bb8db25
d705b01
7798d6b
4b85a98
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,176 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createHmac } from "crypto"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 1 in src/api/functions/subscriberCallback.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { lookup } from "dns/promises"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 2 in src/api/functions/subscriberCallback.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { isIP } from "net"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 3 in src/api/functions/subscriberCallback.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { ValidLoggers } from "api/types.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const PRIVATE_IPV4_RANGES: { cidr: string; mask: number }[] = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { cidr: "10.0.0.0", mask: 8 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Check noticeCode scanning / SonarCloud IP addresses should not be hardcoded Low
Make sure using a hardcoded IP address 10.0.0.0 is safe here. See more on SonarQube Cloud
Check warning on line 7 in src/api/functions/subscriberCallback.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| { cidr: "172.16.0.0", mask: 12 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Check noticeCode scanning / SonarCloud IP addresses should not be hardcoded Low
Make sure using a hardcoded IP address 172.16.0.0 is safe here. See more on SonarQube Cloud
Check warning on line 8 in src/api/functions/subscriberCallback.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| { cidr: "192.168.0.0", mask: 16 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Check noticeCode scanning / SonarCloud IP addresses should not be hardcoded Low
Make sure using a hardcoded IP address 192.168.0.0 is safe here. See more on SonarQube Cloud
Check warning on line 9 in src/api/functions/subscriberCallback.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| { cidr: "127.0.0.0", mask: 8 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { cidr: "169.254.0.0", mask: 16 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Check noticeCode scanning / SonarCloud IP addresses should not be hardcoded Low
Make sure using a hardcoded IP address 169.254.0.0 is safe here. See more on SonarQube Cloud
Check warning on line 11 in src/api/functions/subscriberCallback.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| { cidr: "0.0.0.0", mask: 8 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { cidr: "100.64.0.0", mask: 10 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Check noticeCode scanning / SonarCloud IP addresses should not be hardcoded Low
Make sure using a hardcoded IP address 100.64.0.0 is safe here. See more on SonarQube Cloud
Check warning on line 13 in src/api/functions/subscriberCallback.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+14
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SSRF coverage gap: missing special-use IPv4 ranges. The blocklist omits several non-routable / special-use ranges that should not be valid callback destinations:
Any of these reaching 🛡️ Proposed additions const PRIVATE_IPV4_RANGES: { cidr: string; mask: number }[] = [
{ cidr: "10.0.0.0", mask: 8 },
{ cidr: "172.16.0.0", mask: 12 },
{ cidr: "192.168.0.0", mask: 16 },
{ cidr: "127.0.0.0", mask: 8 },
{ cidr: "169.254.0.0", mask: 16 },
{ cidr: "0.0.0.0", mask: 8 },
{ cidr: "100.64.0.0", mask: 10 },
+ { cidr: "224.0.0.0", mask: 4 }, // multicast
+ { cidr: "240.0.0.0", mask: 4 }, // reserved
+ { cidr: "198.18.0.0", mask: 15 }, // benchmarking
+ { cidr: "192.0.0.0", mask: 24 }, // IETF protocol assignments
+ { cidr: "192.0.2.0", mask: 24 }, // TEST-NET-1
+ { cidr: "198.51.100.0", mask: 24 }, // TEST-NET-2
+ { cidr: "203.0.113.0", mask: 24 }, // TEST-NET-3
];📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: SonarCloud[notice] 7-7: IP addresses should not be hardcoded Make sure using a hardcoded IP address 10.0.0.0 is safe here.See more on SonarQube Cloud [notice] 8-8: IP addresses should not be hardcoded Make sure using a hardcoded IP address 172.16.0.0 is safe here.See more on SonarQube Cloud [notice] 9-9: IP addresses should not be hardcoded Make sure using a hardcoded IP address 192.168.0.0 is safe here.See more on SonarQube Cloud [notice] 11-11: IP addresses should not be hardcoded Make sure using a hardcoded IP address 169.254.0.0 is safe here.See more on SonarQube Cloud [notice] 13-13: IP addresses should not be hardcoded Make sure using a hardcoded IP address 100.64.0.0 is safe here.See more on SonarQube Cloud 🪛 GitHub Check: SonarCloud Code Analysis[warning] 8-8: Make sure using a hardcoded IP address 172.16.0.0 is safe here. [warning] 13-13: Make sure using a hardcoded IP address 100.64.0.0 is safe here. [warning] 9-9: Make sure using a hardcoded IP address 192.168.0.0 is safe here. [warning] 7-7: Make sure using a hardcoded IP address 10.0.0.0 is safe here. [warning] 11-11: Make sure using a hardcoded IP address 169.254.0.0 is safe here. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ipv4ToInt = (ip: string): number => | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ip.split(".").reduce((acc, part) => (acc << 8) + Number(part), 0) >>> 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isPrivateIPv4 = (ip: string): boolean => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ipInt = ipv4ToInt(ip); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return PRIVATE_IPV4_RANGES.some(({ cidr, mask }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cidrInt = ipv4ToInt(cidr); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const maskBits = mask === 0 ? 0 : (~0 << (32 - mask)) >>> 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (ipInt & maskBits) === (cidrInt & maskBits); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isPrivateIPv6 = (ip: string): boolean => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized = ip.toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (normalized === "::1") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (normalized.startsWith("fc") || normalized.startsWith("fd")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; // fc00::/7 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalized.startsWith("fe8") || | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalized.startsWith("fe9") || | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalized.startsWith("fea") || | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalized.startsWith("feb") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; // fe80::/10 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IPv6 SSRF bypass: IPv4-mapped addresses not detected.
Also worth covering for completeness:
🛡️ Suggested approachWhen const isPrivateIPv6 = (ip: string): boolean => {
const normalized = ip.toLowerCase();
+ if (normalized === "::" || normalized === "::1") {
+ return true;
+ }
+ // IPv4-mapped/compat: ::ffff:a.b.c.d or ::ffff:0:a.b.c.d
+ const mappedMatch = normalized.match(
+ /^::(?:ffff(?::0)?:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/,
+ );
+ if (mappedMatch) {
+ return isPrivateIPv4(mappedMatch[1]);
+ }
- if (normalized === "::1") {
- return true;
- }
if (normalized.startsWith("fc") || normalized.startsWith("fd")) {
return true;
}
if (
normalized.startsWith("fe8") ||
normalized.startsWith("fe9") ||
normalized.startsWith("fea") ||
normalized.startsWith("feb")
) {
return true;
}
+ if (normalized.startsWith("2001:db8:") || normalized.startsWith("64:ff9b:")) {
+ return true;
+ }
return false;
};🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class SubscriberCallbackBlockedError extends Error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(message: string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| super(message); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.name = "SubscriberCallbackBlockedError"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const assertCallbackUrlIsExternal = async ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = new URL(url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parsed.protocol !== "https:") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new SubscriberCallbackBlockedError("callbackUrl must use https://"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const host = parsed.hostname.replace(/^\[(.*)\]$/, "$1").toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (host === "localhost") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new SubscriberCallbackBlockedError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "callbackUrl host is not reachable.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ipVersion = isIP(host); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ipVersion === 4 && isPrivateIPv4(host)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new SubscriberCallbackBlockedError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "callbackUrl resolves to a private IPv4 range.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ipVersion === 6 && isPrivateIPv6(host)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new SubscriberCallbackBlockedError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "callbackUrl resolves to a private IPv6 range.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ipVersion !== 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resolved = await lookup(host, { all: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (resolved.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new SubscriberCallbackBlockedError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `callbackUrl host ${host} did not resolve.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const entry of resolved) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.family === 4 && isPrivateIPv4(entry.address)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new SubscriberCallbackBlockedError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `callbackUrl host ${host} resolves to private IPv4 ${entry.address}.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entry.family === 6 && isPrivateIPv6(entry.address)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new SubscriberCallbackBlockedError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `callbackUrl host ${host} resolves to private IPv6 ${entry.address}.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+81
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: To mitigate DNS rebinding attacks in SSRF scenarios when using Node.js undici fetch, create a custom Agent with a safe DNS lookup function in the connect options. This intercepts DNS resolution after the initial lookup, allowing you to validate the resolved IP address against private ranges, loopback, etc., before connecting. This effectively pins and checks the IP, preventing rebinding where a domain resolves to a safe IP first then rebinds to internal IPs.[1][2] Use a library like ipaddr.js for robust IP range classification.[1] Example code: import { Agent } from 'undici'; import { lookup } from 'dns'; import ipaddr from 'ipaddr.js'; const safeLookup = (hostname, options, callback) => { lookup(hostname, options, (err, address, family) => { if (err) { return callback(err, address, family); } if (!ipaddr.isValid(address)) { return callback(new Error('Invalid address'), address, family); } const ipObj = ipaddr.parse(address); if (ipObj.range!== 'unicast') { // Blocks private, loopback, link-local, etc. return callback(new Error('Not Allowed'), address, family); } return callback(null, address, family); }); }; const safeAgent = new Agent({ connect: { lookup: safeLookup, }, }); // Usage with fetch const response = await fetch('http://example.com', { dispatcher: safeAgent }); This approach is recommended in undici discussions for SSRF protection and directly counters DNS rebinding by validating every resolution.[1][3] Undici also has a DNS interceptor (added in recent versions via PR Citations:
🏁 Script executed: # First, find the file and understand its full context
cat -n src/api/functions/subscriberCallback.ts | head -150Repository: acm-uiuc/core Length of output: 5343 🏁 Script executed: # See the complete fetch call and its options
sed -n '145,160p' src/api/functions/subscriberCallback.tsRepository: acm-uiuc/core Length of output: 455 DNS rebinding TOCTOU vulnerability: validated host may resolve to a different IP at fetch time.
Repro pattern: attacker registers a payment link with Recommended mitigation: pin the validated IP and direct import { Agent } from "undici";
// after lookup() + validation, pick one address (e.g., resolved[0])
const dispatcher = new Agent({
connect: { lookup: (_host, opts, cb) => cb(null, validatedIp, validatedFamily) },
});
await fetch(callbackUrl, { /* ... */, dispatcher });Alternatively, use 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const signCallbackBody = ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| body, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| signingSecret, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| signingSecret: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }): string => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hmac = createHmac("sha256", signingSecret); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| hmac.update(`${timestamp}.${body}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return hmac.digest("hex"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type DeliverSubscriberCallbackParams = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| callbackUrl: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| signingSecret: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: object; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| eventId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger: ValidLoggers; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeoutMs?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const deliverSubscriberCallback = async ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| callbackUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| signingSecret, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| body, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| eventId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeoutMs = 5000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: DeliverSubscriberCallbackParams): Promise<void> => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await assertCallbackUrlIsExternal(callbackUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const serialized = JSON.stringify(body); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const timestamp = Math.floor(Date.now() / 1000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const signature = signCallbackBody({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: serialized, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| signingSecret, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const controller = new AbortController(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const timer = setTimeout(() => controller.abort(), timeoutMs); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let response: Response; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = await fetch(callbackUrl, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: serialized, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Content-Type": "application/json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "X-ACM-Signature": `t=${timestamp},v1=${signature}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "X-ACM-Event-Id": eventId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| signal: controller.signal, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearTimeout(timer); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const truncated = await response | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .text() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then((t) => t.slice(0, 256)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch(() => ""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: response.status, callbackUrl, eventId, body: truncated }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Subscriber callback returned non-2xx; will retry.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Subscriber callback returned ${response.status} from ${callbackUrl}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { status: response.status, callbackUrl, eventId }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Subscriber callback delivered.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.