Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.githubci
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,6 @@ OTEL_SERVICE_NAME=COOP_TEST_SERVICE
NODE_ENV=CI

ITEM_QUEUE_TRAFFIC_PERCENTAGE='0'
UI_URL=https://getcoop.com
UI_URL=http://localhost:3000

ENABLE_DEMO_REQUEST=false
11 changes: 10 additions & 1 deletion client/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@
# (e.g., to debug production errors).
# GENERATE_SOURCEMAP=false

# URL of the content proxy service to use for content display in iframes.
# Public-facing base URL of this Coop instance (e.g. https://coop.example.com).
# Used to construct user-facing links such as password reset / invite links and
# API code samples. Leave unset to fall back to the browser's current origin.
VITE_UI_URL=

# Base URL of the published Coop documentation. Override to point at a fork's
# or mirror's docs.
VITE_DOCS_URL=https://roostorg.github.io/coop/latest

# URL of the content proxy service to use for content display in iframes.
# This is used to proxy the content URL to the content proxy service.
VITE_CONTENT_PROXY_URL=http://localhost:4000

Expand Down
31 changes: 31 additions & 0 deletions client/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Centralized URL configuration for the Coop client.
*
* Coop is open source and self-hosted, so any deployment-specific URL must come
* from the deployment's environment rather than being hardcoded. Add new URL
* constants here (sourced from `import.meta.env.VITE_*`) instead of inlining
* literals at call sites.
*/

/**
* Base URL of this Coop instance, used to construct user-facing links such as
* password reset / invite links, dashboard deep-links, and API code samples.
*
* Configure with `VITE_UI_URL` at build time to override the runtime origin
* (useful when the client is served from a different host than its public
* URL, e.g. behind a reverse proxy). Falls back to the browser's current
* origin when unset.
*/
export const HOST_URL: string =
import.meta.env.VITE_UI_URL ?? window.location.origin;

/**
* Base URL of the published Coop documentation site.
*
* Configure with `VITE_DOCS_URL` at build time to point at a fork's or
* mirror's docs. The default value lives in `client/.env.example` and is
* applied via the standard `.env` copy step; this code-level fallback only
* kicks in if the variable is missing entirely from the build environment.
*/
export const DOCS_URL: string =
import.meta.env.VITE_DOCS_URL ?? 'https://roostorg.github.io/coop/latest';
Comment on lines +19 to +31
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle empty env values when resolving base URLs.

On Line 20 and Line 30, ?? won’t fall back if VITE_UI_URL/VITE_DOCS_URL are set to '' (empty string), which can generate broken docs links and relative host links.

Suggested fix
-export const HOST_URL: string =
-  import.meta.env.VITE_UI_URL ?? window.location.origin;
+export const HOST_URL: string =
+  import.meta.env.VITE_UI_URL?.trim() || window.location.origin;
@@
-export const DOCS_URL: string =
-  import.meta.env.VITE_DOCS_URL ?? 'https://roostorg.github.io/coop/latest';
+export const DOCS_URL: string =
+  import.meta.env.VITE_DOCS_URL?.trim() ||
+  'https://roostorg.github.io/coop/latest';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/lib/config.ts` around lines 19 - 30, HOST_URL and DOCS_URL
currently use the nullish coalescing operator (??) which does not treat empty
strings as missing; update the resolution logic for import.meta.env.VITE_UI_URL
and import.meta.env.VITE_DOCS_URL so empty values also fall back (e.g. treat
value as falsy by using a trimmed-string check or logical OR) — change the
expressions that compute HOST_URL and DOCS_URL to check/import.meta.env.VITE_*
variables, trim and test for non-empty before using them, otherwise use
window.location.origin for HOST_URL and the default docs URL for DOCS_URL.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

our linter does not like || having ?? is ok.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juanmrad, understood — I'll withdraw the || suggestion. The ?? pattern is fine for this use case; empty-string env vars are an unusual misconfiguration and the current code is clear. Thanks for the context!

(๑˃ᴗ˂)ﻌﻌﻌ♡


✏️ Learnings added
Learnt from: juanmrad
Repo: roostorg/coop PR: 527
File: client/src/lib/config.ts:19-31
Timestamp: 2026-05-20T03:51:03.594Z
Learning: In the `roostorg/coop` repository, the ESLint (or equivalent) linter is configured to disallow the logical OR (`||`) operator in favour of the nullish coalescing operator (`??`). Do not suggest replacing `??` with `||` for fallback expressions in this codebase.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

3 changes: 2 additions & 1 deletion client/src/webpages/dashboard/actions/ActionForm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DOCS_URL } from '@/lib/config';
import { gql } from '@apollo/client';
import { Input, Select } from 'antd';
import Link from 'antd/lib/typography/Link';
Expand Down Expand Up @@ -322,7 +323,7 @@ export default function ActionForm() {
<span className="font-semibold">Note</span>: For each HTTP request we
send to that URL, we will include a JSON body with information about the
action. See the{' '}
<Link href="https://docs.getcoop.com/docs/action-api">
<Link href={`${DOCS_URL}/api/actions.html`}>
documentation
</Link>{' '}
for more information.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HOST_URL } from '@/lib/config';
import { ItemTypeKind } from '@roostorg/types';
import zipObject from 'lodash/zipObject';

Expand Down Expand Up @@ -163,13 +164,13 @@ function generateCurlRequest(

switch (apiRoute) {
case 'Items API':
return `curl --request POST --url https://getcoop.com/api/v1/items/async --header 'x-api-key: APIKEY' --header 'Content-Type: application/json' --data '${JSON.stringify(
return `curl --request POST --url ${HOST_URL}/api/v1/items/async --header 'x-api-key: APIKEY' --header 'Content-Type: application/json' --data '${JSON.stringify(
data,
null,
4,
)}'`;
case 'Reports API':
return `curl --request POST --url https://getcoop.com/api/v1/report --header 'x-api-key: APIKEY' --header 'Content-Type: application/json' --data '${JSON.stringify(
return `curl --request POST --url ${HOST_URL}/api/v1/report --header 'x-api-key: APIKEY' --header 'Content-Type: application/json' --data '${JSON.stringify(
data,
null,
4,
Expand Down Expand Up @@ -197,7 +198,7 @@ data = ${JSON.stringify(data, null, 4)
.replace(/true/g, 'True')
.replace(/false/g, 'False')}
response = requests.post(
'https://getcoop.com/api/v1/items/async',
'${HOST_URL}/api/v1/items/async',
headers=headers,
json=data
)
Expand All @@ -214,7 +215,7 @@ headers = {
data = ${JSON.stringify(data, null, 4)}

response = requests.post(
'https://getcoop.com/api/v1/report',
'${HOST_URL}/api/v1/report',
headers=headers,
json=data
)
Expand All @@ -236,7 +237,7 @@ function generateNodeJsRequest(
${JSON.stringify(data, null, 4)};

const response = await fetch(
"https://getcoop.com/api/v1/items/async",
"${HOST_URL}/api/v1/items/async",
{
method: 'post',
body: JSON.stringify(body),
Expand All @@ -253,7 +254,7 @@ console.log(response.status);
return `const body = ${JSON.stringify(data, null, 4)}

const response = await fetch(
"https://getcoop.com/api/v1/report",
"${HOST_URL}/api/v1/report",
{
method: 'post',
body: JSON.stringify(body),
Expand Down Expand Up @@ -291,7 +292,7 @@ $headers = [

$body = ${data};

$response = $client->request('POST', 'https://getcoop.com/api/v1/items/async', [
$response = $client->request('POST', '${HOST_URL}/api/v1/items/async', [
'headers' => $headers,
'json' => $body,
]);
Expand All @@ -312,7 +313,7 @@ $headers = [
$body = ${data};


$response = $client->request('POST', 'https://getcoop.com/api/v1/report', [
$response = $client->request('POST', '${HOST_URL}/api/v1/report', [
'headers' => $headers,
'json' => $body,
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DOCS_URL } from '@/lib/config';
import { gql } from '@apollo/client';
import { Input, notification } from 'antd';
import Link from 'antd/lib/typography/Link';
Expand Down Expand Up @@ -117,7 +118,7 @@ export default function ManualReviewAppealSettings() {
<span className="font-semibold">Note</span>: For each HTTP request we
send to that URL, we will include a JSON body with information about the
appeal. See the{' '}
<Link href="https://docs.getcoop.com/docs/appeal-api">
<Link href={`${DOCS_URL}/api/appeal.html`}>
documentation
</Link>{' '}
for more information.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ChevronLeft from '@/icons/lni/Direction/chevron-left.svg?react';
import ChevronRight from '@/icons/lni/Direction/chevron-right.svg?react';
import CrossCircle from '@/icons/lni/Interface and Sign/cross-circle.svg?react';
import { HOST_URL } from '@/lib/config';
import { RedoOutlined } from '@ant-design/icons';
import { gql } from '@apollo/client';
import { Button, Input } from 'antd';
Expand Down Expand Up @@ -576,7 +577,7 @@ export default function ManualReviewRecentDecisions() {
item.reviewer,
item.queue,
item.createdAt,
`https://getcoop.com/dashboard/manual_review/recent?jobId=${item.jobId}`,
`${HOST_URL}/dashboard/manual_review/recent?jobId=${item.jobId}`,
]);

// Combine the headers and rows into a CSV string
Expand Down Expand Up @@ -623,7 +624,7 @@ export default function ManualReviewRecentDecisions() {
getReviewerName(skip.userId),
getQueueName(skip.queueId),
parseDatetimeToReadableStringInUTC(new Date(skip.ts)),
`https://getcoop.com/dashboard/manual_review/recent?jobId=${skip.jobId}`,
`${HOST_URL}/dashboard/manual_review/recent?jobId=${skip.jobId}`,
];
});
// Define the CSV headers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ export default function IframeContentDisplayComponent(props: {
const { contentUrl } = props;

const contentProxyUrl =
import.meta.env.VITE_CONTENT_PROXY_URL ??
(import.meta.env.MODE === 'production'
? 'https://content.getcoop.com'
: 'http://localhost:4000');
import.meta.env.VITE_CONTENT_PROXY_URL?.trim() ??
(import.meta.env.DEV ? 'http://localhost:4000' : window.location.origin);
Comment on lines +18 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In JavaScript/TypeScript, does value?.trim() ?? fallbackusefallbackwhenvalue is an empty string or whitespace-only string?

💡 Result:

No, the expression value?.trim?? fallback does not use the fallback when value is an empty string or a whitespace-only string [1][2][3]. The nullish coalescing operator (??) only triggers its right-hand side (the fallback) when the left-hand side is null or undefined [1][4][5]. Because an empty string ("") and a whitespace-only string (" ") are valid string values—not null or undefined—the expression will return the result of the trim operation instead of the fallback [1][2][3]. Specifically: - If value is "", value?.trim evaluates to "", and the expression returns "" [1][5]. - If value is " ", value?.trim evaluates to "", and the expression returns "" [6]. - If value is null or undefined, value?.trim evaluates to undefined, and the expression returns the fallback [1][7]. If your goal is to treat empty or whitespace-only strings as "missing" and use the fallback in those cases, you must handle that logic explicitly, for example: const result = (value?.trim || null)?? fallback; // Or more simply: const result = (!value?.trim)? fallback: value.trim; [8][3]

Citations:


🏁 Script executed:

# Check if file exists and read the relevant section
if [ -f "client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx" ]; then
  echo "File exists. Reading lines 10-25:"
  sed -n '10,25p' "client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx"
else
  echo "File not found at specified path"
fi

Repository: roostorg/coop

Length of output: 675


🏁 Script executed:

# Search for other uses of VITE_CONTENT_PROXY_URL in codebase
rg "VITE_CONTENT_PROXY_URL" --type ts --type tsx

Repository: roostorg/coop

Length of output: 85


🏁 Script executed:

# Search for contentProxyUrl usage to see how it's used (especially postMessage)
rg "contentProxyUrl" --type ts --type tsx -A 3

Repository: roostorg/coop

Length of output: 85


🏁 Script executed:

# Search for VITE_CONTENT_PROXY_URL without type flag
rg "VITE_CONTENT_PROXY_URL"

Repository: roostorg/coop

Length of output: 197


🏁 Script executed:

# Search for contentProxyUrl usage (especially postMessage and origin checks)
rg "contentProxyUrl" -A 3 -B 1

Repository: roostorg/coop

Length of output: 4112


🏁 Script executed:

# Read the entire file to see all usages and context
cat -n "client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx" | head -100

Repository: roostorg/coop

Length of output: 3757


Handle empty VITE_CONTENT_PROXY_URL before fallback.

At line 18-19, ?.trim() ?? ... won't fall back when the env var is '' or whitespace-only. The nullish coalescing operator (??) only triggers on null or undefined, not empty strings, so contentProxyUrl can become an empty string. This breaks origin validation in the message handler (line 53) where event.origin !== "" always rejects incoming messages, and invalidates the postMessage target origin at lines 77 and 93.

Suggested fix
-  const contentProxyUrl =
-    import.meta.env.VITE_CONTENT_PROXY_URL?.trim() ??
-    (import.meta.env.DEV ? 'http://localhost:4000' : window.location.origin);
+  const configuredContentProxyUrl =
+    import.meta.env.VITE_CONTENT_PROXY_URL?.trim();
+  const contentProxyUrl =
+    configuredContentProxyUrl?.length
+      ? configuredContentProxyUrl
+      : import.meta.env.DEV
+        ? 'http://localhost:4000'
+        : window.location.origin;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import.meta.env.VITE_CONTENT_PROXY_URL?.trim() ??
(import.meta.env.DEV ? 'http://localhost:4000' : window.location.origin);
const configuredContentProxyUrl =
import.meta.env.VITE_CONTENT_PROXY_URL?.trim();
const contentProxyUrl =
configuredContentProxyUrl?.length
? configuredContentProxyUrl
: import.meta.env.DEV
? 'http://localhost:4000'
: window.location.origin;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx`
around lines 18 - 19, The environment variable parsing for contentProxyUrl
allows empty or whitespace-only values because it uses
import.meta.env.VITE_CONTENT_PROXY_URL?.trim() ?? fallback; change the logic
that sets contentProxyUrl to treat an empty string (after trim) as missing by
using a truthy check (e.g., const contentProxyUrl =
(import.meta.env.VITE_CONTENT_PROXY_URL || '').trim() || (import.meta.env.DEV ?
'http://localhost:4000' : window.location.origin)); then update any code relying
on contentProxyUrl (the message event handler that checks event.origin and the
postMessage calls that use contentProxyUrl as the target origin) to use the
sanitized contentProxyUrl so origin validation and postMessage target origins
are never an empty string.


const [isIframeLoading, setIsIframeLoading] = useState(true);
const [isTranslating, setIsTranslating] = useState(false);
Expand Down
8 changes: 3 additions & 5 deletions client/src/webpages/settings/ManageUsers.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HOST_URL } from '@/lib/config';
import { gql } from '@apollo/client';
import { Select } from 'antd';
import { MouseEvent, useCallback, useMemo, useState } from 'react';
Expand Down Expand Up @@ -218,8 +219,7 @@ export default function ManageUsers() {

const copyPasswordResetLink = () => {
if (passwordResetToken) {
const uiUrl = import.meta.env.VITE_UI_URL ?? window.location.origin;
const resetUrl = `${uiUrl}/reset_password/${passwordResetToken}`;
const resetUrl = `${HOST_URL}/reset_password/${passwordResetToken}`;
navigator.clipboard.writeText(resetUrl);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
Expand Down Expand Up @@ -542,9 +542,7 @@ export default function ManageUsers() {
<input
type="text"
readOnly
value={`${
import.meta.env.VITE_UI_URL ?? window.location.origin
}/reset_password/${passwordResetToken}`}
value={`${HOST_URL}/reset_password/${passwordResetToken}`}
className="flex-1 px-3 py-2 border border-gray-300 rounded text-sm font-mono bg-gray-50"
onClick={(e) => e.currentTarget.select()}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useGQLHasNcmecReportingEnabledQuery,
useGQLInviteUserMutation,
} from '@/graphql/generated';
import { HOST_URL } from '@/lib/config';
import { titleCaseEnumString } from '@/utils/string';
import { gql } from '@apollo/client';
import { useState } from 'react';
Expand Down Expand Up @@ -78,8 +79,7 @@ export default function ManageUsersInviteUserSection() {

const copyInviteLink = () => {
if (inviteToken) {
const uiUrl = import.meta.env.VITE_UI_URL ?? window.location.origin;
const signupUrl = `${uiUrl}/signup/${inviteToken}`;
const signupUrl = `${HOST_URL}/signup/${inviteToken}`;
navigator.clipboard.writeText(signupUrl);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
Expand Down Expand Up @@ -118,9 +118,7 @@ export default function ManageUsersInviteUserSection() {
<input
type="text"
readOnly
value={`${
import.meta.env.VITE_UI_URL ?? window.location.origin
}/signup/${inviteToken}`}
value={`${HOST_URL}/signup/${inviteToken}`}
className="flex-1 px-3 py-2 border border-gray-300 rounded text-sm font-mono bg-gray-50"
onClick={(e) => e.currentTarget.select()}
/>
Expand Down
7 changes: 4 additions & 3 deletions client/src/webpages/settings/SSOSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Textarea } from '@/coop-ui/Textarea';
import { toast } from '@/coop-ui/Toast';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/coop-ui/Tooltip';
import { Heading, Text } from '@/coop-ui/Typography';
import { HOST_URL } from '@/lib/config';
import { userHasPermissions } from '@/routing/permissions';
import { gql } from '@apollo/client';
import { Clipboard } from 'lucide-react';
Expand Down Expand Up @@ -76,7 +77,7 @@ export default function SSOSettings() {
const copyText = (text: string) => {
navigator.clipboard.writeText(text);
};
const callbackUri = `https://getcoop.com/api/v1/saml/login/${data?.myOrg?.id}/callback`;
const callbackUri = `${HOST_URL}/api/v1/saml/login/${data?.myOrg?.id}/callback`;

return (
<div className="flex flex-col w-3/5 gap-4 text-start">
Expand Down Expand Up @@ -122,7 +123,7 @@ export default function SSOSettings() {
id="SpEntityId"
type={'text'}
className={'tracking-widest'}
value={'https://getcoop.com'}
value={HOST_URL}
disabled
endSlot={
<div className="flex">
Expand All @@ -132,7 +133,7 @@ export default function SSOSettings() {
variant="white"
size="icon"
className="h-[2.875rem] rounded-none rounded-r-lg border-l-0"
onClick={() => copyText('https://getcoop.com')}
onClick={() => copyText(HOST_URL)}
>
<Clipboard />
</Button>
Expand Down
37 changes: 18 additions & 19 deletions server/utils/url.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { validateUrl } from './url.js';

describe('URL Tests', () => {
describe('Deny Prod URLs', () => {
describe('Loopback gating (default)', () => {
beforeEach(() => {
// This absolutely is unsafe mutation of a global that'll be visible
// across test suites. However, this env var should only be relied upon
Expand All @@ -13,24 +13,18 @@ describe('URL Tests', () => {
delete process.env.ALLOW_USER_INPUT_LOCALHOST_URIS;
});

test('Deny Coop domains', () => {
expect(() => validateUrl('https://www.coopapi.com')).toThrow();
expect(() => validateUrl('https://www.trycoop.co')).toThrow();
expect(() => validateUrl('https://www.getcoop.com')).toThrow();
});

test('Deny localhost domains', () => {
// This absolutely is unsafe mutation of a global that'll be visible
// across test suites. However, this env var should only be relied upon
// by this module, so it should be ok.
process.env.ALLOW_USER_INPUT_LOCALHOST_URIS = 'false';

expect(() => validateUrl('https://localhost:3000')).toThrow();
expect(() => validateUrl('https://127.0.0.1')).toThrow();
});

test('Allow arbitrary external URLs', () => {
expect(() => validateUrl('https://example.com')).not.toThrow();
expect(() => validateUrl('https://api.example.org/webhook')).not.toThrow();
});
});

describe('Allow development URLs', () => {
describe('Loopback gating (development override)', () => {
beforeEach(() => {
// This absolutely is unsafe mutation of a global that'll be visible
// across test suites. However, this env var should only be relied upon
Expand All @@ -42,15 +36,20 @@ describe('URL Tests', () => {
delete process.env.ALLOW_USER_INPUT_LOCALHOST_URIS;
});

test('Deny Coop domains', () => {
expect(() => validateUrl('https://www.coopapi.com')).toThrow();
expect(() => validateUrl('https://www.trycoop.co')).toThrow();
expect(() => validateUrl('https://www.getcoop.com')).toThrow();
});

test('Allow localhost domains', () => {
expect(() => validateUrl('https://localhost:3000')).not.toThrow();
expect(() => validateUrl('https://127.0.0.1')).not.toThrow();
});
});

describe('Caller-provided blockedHostnames', () => {
test('Honor custom blocklist passed via opts', () => {
expect(() =>
validateUrl('https://blocked.example.com', {
allowedSchemes: ['http', 'https'],
blockedHostnames: ['blocked.example.com'],
}),
).toThrow();
});
});
});
17 changes: 6 additions & 11 deletions server/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,12 @@ type UrlValidationOptions = {
function defaultBlockedHostnames(): string[] {
const { ALLOW_USER_INPUT_LOCALHOST_URIS } = process.env;

return [
'coopapi.com',
'www.coopapi.com',
'trycoop.co',
'www.trycoop.co',
'getcoop.com',
'www.getcoop.com',
...(ALLOW_USER_INPUT_LOCALHOST_URIS === 'true'
? []
: ['localhost', '127.0.0.1']),
];
// Block loopback addresses to reduce SSRF risk from user-supplied URLs (e.g.
// webhook callbacks). Operators who want stricter blocking against their own
// public hostname should pass `blockedHostnames` via `UrlValidationOptions`.
return ALLOW_USER_INPUT_LOCALHOST_URIS === 'true'
? []
: ['localhost', '127.0.0.1'];
}

export function validateUrl(
Expand Down
Loading