Skip to content
Closed
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
7 changes: 6 additions & 1 deletion cli/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ function generateOperationId(): string {
}

const corsOptions: CorsOptions = {
origin: ['https://npmx.dev', /^http:\/\/localhost:\d+$/, /^http:\/\/127.0.0.1:\d+$/],
origin: [
'https://npmx.dev',
/^http:\/\/localhost:\d+$/,
/^http:\/\/127.0.0.1:\d+$/,
/^https:\/\/(.*)\.vercel\.app$/,
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

🧩 Analysis chain

🏁 Script executed:

head -120 cli/src/server.ts | tail -40

Repository: npmx-dev/npmx.dev

Length of output: 1044


🏁 Script executed:

find . -name "vercel.json" -o -name ".vercelignore" | head -5

Repository: npmx-dev/npmx.dev

Length of output: 74


🏁 Script executed:

grep -n "vercel" package.json | head -10

Repository: npmx-dev/npmx.dev

Length of output: 43


🌐 Web query:

Vercel preview deployment URL pattern subdomain structure and how to scope CORS to specific projects or teams

💡 Result:

Vercel preview deployment URL patterns (default .vercel.app)

Vercel “generated URLs” are built from these components: <project-name>, <scope-slug> (team/account slug), optional <branch-name>, and optional <unique-hash>. (vercel.com)

Common preview URL structures from Git: (vercel.com)

  • Commit-specific preview (changes for one commit):
    https://<project-name>-<unique-hash>-<scope-slug>.vercel.app
  • Branch preview (always latest for that branch):
    https://<project-name>-git-<branch-name>-<scope-slug>.vercel.app

(There are also CLI-generated forms like https://<project-name>-<scope-slug>.vercel.app and sometimes ...-<author-name>-... for team deployments.) (vercel.com)

Making preview URLs predictable (best for CORS allowlists)

If you need a stable domain pattern, use Preview Deployment Suffix to replace .vercel.app with a domain you control (configured at the team level, and the domain must exist/be active in that team). (vercel.com)
Example outcome: previews become something like https://<project>...<team>.<your-preview-suffix-domain> instead of ...vercel.app. (vercel.com)

If you want wildcard-style origins (e.g. https://*.preview.example.com), you typically pair this with Vercel-managed DNS / wildcard domain handling so subdomains resolve cleanly. (vercel.com)

How to scope CORS to specific Vercel projects/teams

CORS itself doesn’t understand “Vercel project/team”—it only sees the request Origin (scheme + host + port). So you scope by matching the Origin host to the project/team patterns you allow.

Option A (works with default .vercel.app): validate origin by suffix + project prefix

Use a dynamic origin check on your API:

  • Require host ends with: -<scope-slug>.vercel.app (scopes to one team/account)
  • Also require host starts with: <project-name>- (scopes to one project)
  • Optionally allow only the -git- (branch) form and/or the -<unique-hash>- (commit) form. (vercel.com)

Example (Node/Next API-style logic):

const allowedScope = "my-team-slug";
const allowedProject = "my-project";

function isAllowedVercelPreviewOrigin(origin) {
  try {
    const { protocol, hostname } = new URL(origin);
    if (protocol !== "https:") return false;

    // team/account scope
    if (!hostname.endsWith(`-${allowedScope}.vercel.app`)) return false;

    // project scope
    if (!hostname.startsWith(`${allowedProject}-`)) return false;

    // optional: ensure it matches either branch or commit style
    const ok =
      hostname.includes(`-git-`) || /-[0-9a-z]{9}-/.test(hostname); // "unique-hash" is 9 chars
    return ok;
  } catch {
    return false;
  }
}

(That 9-character “unique-hash” length is documented.) (vercel.com)

Option B (recommended for strict allowlists): use a custom Preview Deployment Suffix domain per team

With a suffix like preview.example.com, you can allow:

  • https://*.preview.example.com (broad to that team’s preview domain), or
  • https://<project-pattern>.preview.example.com (narrower), depending on how your generated hostnames look after the suffix change. (vercel.com)

Option C: if Deployment Protection breaks CORS preflights

If your previews are protected (Vercel Authentication / Password / Trusted IPs), browser OPTIONS preflights can be blocked. Vercel documents using OPTIONS Allowlist so preflights can succeed (commonly for /api/*) while keeping real requests protected. (vercel.com)

If you tell me your team slug, project name, and whether you’re using default .vercel.app vs Preview Deployment Suffix, I can give you an exact regex/implementation (and for which environments: preview only vs preview+prod).

Citations:


🏁 Script executed:

cat vercel.json | jq . 2>/dev/null || cat vercel.json

Repository: npmx-dev/npmx.dev

Length of output: 2099


🏁 Script executed:

rg -i "team|scope|slug" --type json --type ts -C 2 | head -30

Repository: npmx-dev/npmx.dev

Length of output: 1796


Scope the Vercel CORS origin pattern to your team's deployments.

The regex /^https:\/\/(.*)\.vercel\.app$/ allows CORS requests from any Vercel deployment (across all users and teams). Whilst token validation provides secondary protection, CORS should still follow the defence-in-depth principle.

Vercel preview URLs include the team/account scope slug. Scope your pattern to match your deployments only:

/^https:\/\/npmx-dev-.*-<your-team-slug>\.vercel\.app$/

Replace <your-team-slug> with your Vercel team's slug (visible in your team settings or Vercel preview URLs). This ensures only your team's deployments can request CORS access, not arbitrary Vercel apps from other users or organisations.

],
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
}
Expand Down
Loading