Deployment protection bypass (#298)

* test preview bypass 2

Co-authored-by: Cursor <cursoragent@cursor.com>

* add apiFetch wrapper with Vercel bypass via query param + header

the template workflow was missing VERCEL_AUTOMATION_BYPASS_SECRET,
so all action API calls to preview deployments hit Vercel's
deployment protection without bypass. this also consolidates the
bypass logic into a single fetch wrapper that applies the secret
as both a query parameter (matching server-side forwarding) and
a header for belt-and-suspenders reliability.

Co-authored-by: Cursor <cursoragent@cursor.com>

* security hardening for Vercel bypass

- redact bypass token from webhook forwarder logs and response body
- remove dead x-preview-api-forward header
- refactor getAllSecrets() to use SENSITIVE_PATTERNS instead of hardcoded list
- enforce https:// on API_URL (localhost exempt for local dev)

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Colin McDonnell
2026-02-13 20:01:48 +00:00
committed by pullfrog[bot]
parent d5508d99bb
commit 6f108237d4
10 changed files with 187 additions and 139 deletions
+6 -19
View File
@@ -2,7 +2,7 @@ import { createSign } from "node:crypto";
import * as core from "@actions/core";
import { throttling } from "@octokit/plugin-throttling";
import { Octokit } from "@octokit/rest";
import { getApiUrl, getVercelBypassHeaders } from "./apiUrl.ts";
import { apiFetch } from "./apiFetch.ts";
import { retry } from "./retry.ts";
export interface InstallationToken {
@@ -85,41 +85,28 @@ type AcquireTokenOptions = {
async function acquireTokenViaOIDC(opts?: AcquireTokenOptions): Promise<string> {
const oidcToken = await core.getIDToken("pullfrog-api");
const apiUrl = getApiUrl();
const params = new URLSearchParams();
// ensure the token covers GITHUB_REPOSITORY (may differ from OIDC claims repo)
const repos = [...(opts?.repos ?? [])];
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
if (targetRepo) {
repos.push(targetRepo);
}
if (repos.length) {
params.set("repos", repos.join(","));
}
const queryString = params.toString() ? `?${params.toString()}` : "";
const reposParam = repos.length ? `?repos=${repos.join(",")}` : "";
const timeoutMs = 30000;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const fetchOptions: RequestInit = {
const tokenResponse = await apiFetch({
path: `/api/github/installation-token${reposParam}`,
method: "POST",
headers: {
Authorization: `Bearer ${oidcToken}`,
"Content-Type": "application/json",
...getVercelBypassHeaders(),
},
body: opts?.permissions ? JSON.stringify({ permissions: opts.permissions }) : undefined,
signal: controller.signal,
};
if (opts?.permissions) {
fetchOptions.body = JSON.stringify({ permissions: opts.permissions });
}
const tokenResponse = await fetch(
`${apiUrl}/api/github/installation-token${queryString}`,
fetchOptions
);
});
clearTimeout(timeoutId);