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:
committed by
pullfrog[bot]
co-authored by
Cursor
parent
d5508d99bb
commit
6f108237d4
@@ -32,6 +32,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
prompt: ${{ inputs.prompt }}
|
prompt: ${{ inputs.prompt }}
|
||||||
env:
|
env:
|
||||||
|
API_URL: ${{ secrets.API_URL }}
|
||||||
|
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
|
||||||
# add any additional keys your agent(s) need
|
# add any additional keys your agent(s) need
|
||||||
# optionally, comment out any you won't use
|
# optionally, comment out any you won't use
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- test preview system --> <!-- trivial touch -->
|
<!-- test preview system --> <!-- test bypass 2 -->
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<picture>
|
<picture>
|
||||||
|
|||||||
@@ -139417,15 +139417,43 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
|
|||||||
);
|
);
|
||||||
|
|
||||||
// utils/apiUrl.ts
|
// utils/apiUrl.ts
|
||||||
function getApiUrl() {
|
function isLocalUrl(url4) {
|
||||||
const url4 = process.env.API_URL || "https://pullfrog.com";
|
return url4.hostname === "localhost" || url4.hostname === "127.0.0.1";
|
||||||
log.debug(`resolved API_URL: ${url4}`);
|
|
||||||
return url4;
|
|
||||||
}
|
}
|
||||||
function getVercelBypassHeaders() {
|
function getApiUrl() {
|
||||||
const secret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
const raw = process.env.API_URL || "https://pullfrog.com";
|
||||||
if (!secret) return {};
|
const parsed2 = new URL(raw);
|
||||||
return { "x-vercel-protection-bypass": secret };
|
if (parsed2.protocol !== "https:" && !isLocalUrl(parsed2)) {
|
||||||
|
throw new Error(
|
||||||
|
`API_URL must use https:// (got ${parsed2.protocol}). only localhost is exempt.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.debug(`resolved API_URL: ${raw}`);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// utils/apiFetch.ts
|
||||||
|
async function apiFetch(options) {
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
const url4 = new URL(options.path, apiUrl);
|
||||||
|
const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
||||||
|
if (bypassSecret) {
|
||||||
|
url4.searchParams.set("x-vercel-protection-bypass", bypassSecret);
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
...options.headers
|
||||||
|
};
|
||||||
|
if (bypassSecret) {
|
||||||
|
headers["x-vercel-protection-bypass"] = bypassSecret;
|
||||||
|
}
|
||||||
|
log.debug(`api fetch: ${options.method ?? "GET"} ${url4.pathname}`);
|
||||||
|
const init = {
|
||||||
|
method: options.method ?? "GET",
|
||||||
|
headers
|
||||||
|
};
|
||||||
|
if (options.body) init.body = options.body;
|
||||||
|
if (options.signal) init.signal = options.signal;
|
||||||
|
return fetch(url4.toString(), init);
|
||||||
}
|
}
|
||||||
|
|
||||||
// utils/retry.ts
|
// utils/retry.ts
|
||||||
@@ -139465,37 +139493,26 @@ function isOIDCAvailable() {
|
|||||||
}
|
}
|
||||||
async function acquireTokenViaOIDC(opts) {
|
async function acquireTokenViaOIDC(opts) {
|
||||||
const oidcToken = await core2.getIDToken("pullfrog-api");
|
const oidcToken = await core2.getIDToken("pullfrog-api");
|
||||||
const apiUrl = getApiUrl();
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
const repos = [...opts?.repos ?? []];
|
const repos = [...opts?.repos ?? []];
|
||||||
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
||||||
if (targetRepo) {
|
if (targetRepo) {
|
||||||
repos.push(targetRepo);
|
repos.push(targetRepo);
|
||||||
}
|
}
|
||||||
if (repos.length) {
|
const reposParam = repos.length ? `?repos=${repos.join(",")}` : "";
|
||||||
params.set("repos", repos.join(","));
|
|
||||||
}
|
|
||||||
const queryString = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
const timeoutMs = 3e4;
|
const timeoutMs = 3e4;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
try {
|
try {
|
||||||
const fetchOptions = {
|
const tokenResponse = await apiFetch({
|
||||||
|
path: `/api/github/installation-token${reposParam}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${oidcToken}`,
|
Authorization: `Bearer ${oidcToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json"
|
||||||
...getVercelBypassHeaders()
|
|
||||||
},
|
},
|
||||||
|
body: opts?.permissions ? JSON.stringify({ permissions: opts.permissions }) : void 0,
|
||||||
signal: controller.signal
|
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);
|
clearTimeout(timeoutId);
|
||||||
if (!tokenResponse.ok) {
|
if (!tokenResponse.ok) {
|
||||||
throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
|
throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
|
||||||
@@ -144006,13 +144023,12 @@ function UploadFileTool(ctx) {
|
|||||||
const contentLength = buffer.length;
|
const contentLength = buffer.length;
|
||||||
const fileType = await fileTypeFromBuffer(buffer);
|
const fileType = await fileTypeFromBuffer(buffer);
|
||||||
const contentType = fileType?.mime || "application/octet-stream";
|
const contentType = fileType?.mime || "application/octet-stream";
|
||||||
const apiUrl = getApiUrl();
|
const response = await apiFetch({
|
||||||
const response = await fetch(`${apiUrl}/api/upload/signed-url`, {
|
path: "/api/upload/signed-url",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.apiToken}`,
|
Authorization: `Bearer ${ctx.apiToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json"
|
||||||
...getVercelBypassHeaders()
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
filename,
|
filename,
|
||||||
@@ -146718,23 +146734,18 @@ var defaultRunContext = {
|
|||||||
apiToken: ""
|
apiToken: ""
|
||||||
};
|
};
|
||||||
async function fetchRunContext(params) {
|
async function fetchRunContext(params) {
|
||||||
const apiUrl = getApiUrl();
|
|
||||||
const timeoutMs = 3e4;
|
const timeoutMs = 3e4;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await apiFetch({
|
||||||
`${apiUrl}/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
|
path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${params.token}`,
|
Authorization: `Bearer ${params.token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json"
|
||||||
...getVercelBypassHeaders()
|
|
||||||
},
|
},
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
}
|
});
|
||||||
);
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return defaultRunContext;
|
return defaultRunContext;
|
||||||
|
|||||||
@@ -25681,15 +25681,43 @@ var core2 = __toESM(require_core(), 1);
|
|||||||
import { createSign } from "node:crypto";
|
import { createSign } from "node:crypto";
|
||||||
|
|
||||||
// utils/apiUrl.ts
|
// utils/apiUrl.ts
|
||||||
function getApiUrl() {
|
function isLocalUrl(url) {
|
||||||
const url = process.env.API_URL || "https://pullfrog.com";
|
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
||||||
log.debug(`resolved API_URL: ${url}`);
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
function getVercelBypassHeaders() {
|
function getApiUrl() {
|
||||||
const secret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
const raw = process.env.API_URL || "https://pullfrog.com";
|
||||||
if (!secret) return {};
|
const parsed = new URL(raw);
|
||||||
return { "x-vercel-protection-bypass": secret };
|
if (parsed.protocol !== "https:" && !isLocalUrl(parsed)) {
|
||||||
|
throw new Error(
|
||||||
|
`API_URL must use https:// (got ${parsed.protocol}). only localhost is exempt.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log.debug(`resolved API_URL: ${raw}`);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// utils/apiFetch.ts
|
||||||
|
async function apiFetch(options) {
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
const url = new URL(options.path, apiUrl);
|
||||||
|
const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
||||||
|
if (bypassSecret) {
|
||||||
|
url.searchParams.set("x-vercel-protection-bypass", bypassSecret);
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
...options.headers
|
||||||
|
};
|
||||||
|
if (bypassSecret) {
|
||||||
|
headers["x-vercel-protection-bypass"] = bypassSecret;
|
||||||
|
}
|
||||||
|
log.debug(`api fetch: ${options.method ?? "GET"} ${url.pathname}`);
|
||||||
|
const init = {
|
||||||
|
method: options.method ?? "GET",
|
||||||
|
headers
|
||||||
|
};
|
||||||
|
if (options.body) init.body = options.body;
|
||||||
|
if (options.signal) init.signal = options.signal;
|
||||||
|
return fetch(url.toString(), init);
|
||||||
}
|
}
|
||||||
|
|
||||||
// utils/retry.ts
|
// utils/retry.ts
|
||||||
@@ -25729,37 +25757,26 @@ function isOIDCAvailable() {
|
|||||||
}
|
}
|
||||||
async function acquireTokenViaOIDC(opts) {
|
async function acquireTokenViaOIDC(opts) {
|
||||||
const oidcToken = await core2.getIDToken("pullfrog-api");
|
const oidcToken = await core2.getIDToken("pullfrog-api");
|
||||||
const apiUrl = getApiUrl();
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
const repos = [...opts?.repos ?? []];
|
const repos = [...opts?.repos ?? []];
|
||||||
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
||||||
if (targetRepo) {
|
if (targetRepo) {
|
||||||
repos.push(targetRepo);
|
repos.push(targetRepo);
|
||||||
}
|
}
|
||||||
if (repos.length) {
|
const reposParam = repos.length ? `?repos=${repos.join(",")}` : "";
|
||||||
params.set("repos", repos.join(","));
|
|
||||||
}
|
|
||||||
const queryString = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
const timeoutMs = 3e4;
|
const timeoutMs = 3e4;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
try {
|
try {
|
||||||
const fetchOptions = {
|
const tokenResponse = await apiFetch({
|
||||||
|
path: `/api/github/installation-token${reposParam}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${oidcToken}`,
|
Authorization: `Bearer ${oidcToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json"
|
||||||
...getVercelBypassHeaders()
|
|
||||||
},
|
},
|
||||||
|
body: opts?.permissions ? JSON.stringify({ permissions: opts.permissions }) : void 0,
|
||||||
signal: controller.signal
|
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);
|
clearTimeout(timeoutId);
|
||||||
if (!tokenResponse.ok) {
|
if (!tokenResponse.ok) {
|
||||||
throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
|
throw new Error(`Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText}`);
|
||||||
|
|||||||
+3
-5
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { fileTypeFromBuffer } from "file-type";
|
import { fileTypeFromBuffer } from "file-type";
|
||||||
import { getApiUrl, getVercelBypassHeaders } from "../utils/apiUrl.ts";
|
import { apiFetch } from "../utils/apiFetch.ts";
|
||||||
import type { ToolContext } from "./server.ts";
|
import type { ToolContext } from "./server.ts";
|
||||||
import { execute, tool } from "./shared.ts";
|
import { execute, tool } from "./shared.ts";
|
||||||
|
|
||||||
@@ -25,14 +25,12 @@ export function UploadFileTool(ctx: ToolContext) {
|
|||||||
const fileType = await fileTypeFromBuffer(buffer);
|
const fileType = await fileTypeFromBuffer(buffer);
|
||||||
const contentType = fileType?.mime || "application/octet-stream";
|
const contentType = fileType?.mime || "application/octet-stream";
|
||||||
|
|
||||||
const apiUrl = getApiUrl();
|
const response = await apiFetch({
|
||||||
|
path: "/api/upload/signed-url",
|
||||||
const response = await fetch(`${apiUrl}/api/upload/signed-url`, {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${ctx.apiToken}`,
|
Authorization: `Bearer ${ctx.apiToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...getVercelBypassHeaders(),
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
filename,
|
filename,
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { getApiUrl } from "./apiUrl.ts";
|
||||||
|
import { log } from "./cli.ts";
|
||||||
|
|
||||||
|
type ApiFetchOptions = {
|
||||||
|
path: string;
|
||||||
|
method?: string | undefined;
|
||||||
|
headers?: Record<string, string> | undefined;
|
||||||
|
body?: string | undefined;
|
||||||
|
signal?: AbortSignal | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch wrapper for hitting the Pullfrog API with Vercel deployment protection bypass.
|
||||||
|
*
|
||||||
|
* adds the bypass secret as BOTH a query parameter and a header for maximum reliability.
|
||||||
|
* the server-side forwarding code uses query params, and the Vercel docs say both work,
|
||||||
|
* so we do both as belt-and-suspenders.
|
||||||
|
*
|
||||||
|
* the query param approach is the primary bypass mechanism (matches server-side forwarding).
|
||||||
|
* the header is added as a fallback.
|
||||||
|
*/
|
||||||
|
export async function apiFetch(options: ApiFetchOptions): Promise<Response> {
|
||||||
|
const apiUrl = getApiUrl();
|
||||||
|
const url = new URL(options.path, apiUrl);
|
||||||
|
|
||||||
|
const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
||||||
|
if (bypassSecret) {
|
||||||
|
url.searchParams.set("x-vercel-protection-bypass", bypassSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
// also add as header for belt-and-suspenders
|
||||||
|
if (bypassSecret) {
|
||||||
|
headers["x-vercel-protection-bypass"] = bypassSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug(`api fetch: ${options.method ?? "GET"} ${url.pathname}`);
|
||||||
|
|
||||||
|
const init: RequestInit = {
|
||||||
|
method: options.method ?? "GET",
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
if (options.body) init.body = options.body;
|
||||||
|
if (options.signal) init.signal = options.signal;
|
||||||
|
|
||||||
|
return fetch(url.toString(), init);
|
||||||
|
}
|
||||||
+15
-12
@@ -1,24 +1,27 @@
|
|||||||
import { log } from "./cli.ts";
|
import { log } from "./cli.ts";
|
||||||
|
|
||||||
|
function isLocalUrl(url: URL): boolean {
|
||||||
|
return url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* resolve the Pullfrog API base URL.
|
* resolve the Pullfrog API base URL.
|
||||||
*
|
*
|
||||||
* in the action: API_URL is not explicitly set, so this falls back to https://pullfrog.com.
|
* in the action: API_URL is not explicitly set, so this falls back to https://pullfrog.com.
|
||||||
* in local dev: API_URL=http://localhost:3000 (from .env).
|
* in local dev: API_URL=http://localhost:3000 (from .env).
|
||||||
|
*
|
||||||
|
* enforces https:// for non-local URLs to prevent cleartext credential transmission.
|
||||||
*/
|
*/
|
||||||
export function getApiUrl(): string {
|
export function getApiUrl(): string {
|
||||||
const url = process.env.API_URL || "https://pullfrog.com";
|
const raw = process.env.API_URL || "https://pullfrog.com";
|
||||||
log.debug(`resolved API_URL: ${url}`);
|
const parsed = new URL(raw);
|
||||||
return url;
|
|
||||||
|
if (parsed.protocol !== "https:" && !isLocalUrl(parsed)) {
|
||||||
|
throw new Error(
|
||||||
|
`API_URL must use https:// (got ${parsed.protocol}). only localhost is exempt.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
log.debug(`resolved API_URL: ${raw}`);
|
||||||
* returns headers needed to bypass Vercel deployment protection on preview deployments.
|
return raw;
|
||||||
* when VERCEL_AUTOMATION_BYPASS_SECRET is set (preview repos), includes the bypass header.
|
|
||||||
* otherwise returns an empty object (production / local dev).
|
|
||||||
*/
|
|
||||||
export function getVercelBypassHeaders(): Record<string, string> {
|
|
||||||
const secret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
||||||
if (!secret) return {};
|
|
||||||
return { "x-vercel-protection-bypass": secret };
|
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-19
@@ -2,7 +2,7 @@ import { createSign } from "node:crypto";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { throttling } from "@octokit/plugin-throttling";
|
import { throttling } from "@octokit/plugin-throttling";
|
||||||
import { Octokit } from "@octokit/rest";
|
import { Octokit } from "@octokit/rest";
|
||||||
import { getApiUrl, getVercelBypassHeaders } from "./apiUrl.ts";
|
import { apiFetch } from "./apiFetch.ts";
|
||||||
import { retry } from "./retry.ts";
|
import { retry } from "./retry.ts";
|
||||||
|
|
||||||
export interface InstallationToken {
|
export interface InstallationToken {
|
||||||
@@ -85,41 +85,28 @@ type AcquireTokenOptions = {
|
|||||||
async function acquireTokenViaOIDC(opts?: AcquireTokenOptions): Promise<string> {
|
async function acquireTokenViaOIDC(opts?: AcquireTokenOptions): Promise<string> {
|
||||||
const oidcToken = await core.getIDToken("pullfrog-api");
|
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 repos = [...(opts?.repos ?? [])];
|
||||||
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
const targetRepo = process.env.GITHUB_REPOSITORY?.split("/")[1];
|
||||||
if (targetRepo) {
|
if (targetRepo) {
|
||||||
repos.push(targetRepo);
|
repos.push(targetRepo);
|
||||||
}
|
}
|
||||||
if (repos.length) {
|
const reposParam = repos.length ? `?repos=${repos.join(",")}` : "";
|
||||||
params.set("repos", repos.join(","));
|
|
||||||
}
|
|
||||||
const queryString = params.toString() ? `?${params.toString()}` : "";
|
|
||||||
|
|
||||||
const timeoutMs = 30000;
|
const timeoutMs = 30000;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fetchOptions: RequestInit = {
|
const tokenResponse = await apiFetch({
|
||||||
|
path: `/api/github/installation-token${reposParam}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${oidcToken}`,
|
Authorization: `Bearer ${oidcToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...getVercelBypassHeaders(),
|
|
||||||
},
|
},
|
||||||
|
body: opts?.permissions ? JSON.stringify({ permissions: opts.permissions }) : undefined,
|
||||||
signal: controller.signal,
|
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);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
|||||||
+4
-9
@@ -1,5 +1,5 @@
|
|||||||
import type { AgentName, BashPermission, PushPermission, ToolPermission } from "../external.ts";
|
import type { AgentName, BashPermission, PushPermission, ToolPermission } from "../external.ts";
|
||||||
import { getApiUrl, getVercelBypassHeaders } from "./apiUrl.ts";
|
import { apiFetch } from "./apiFetch.ts";
|
||||||
import type { RepoContext } from "./github.ts";
|
import type { RepoContext } from "./github.ts";
|
||||||
|
|
||||||
export interface Mode {
|
export interface Mode {
|
||||||
@@ -52,24 +52,19 @@ export async function fetchRunContext(params: {
|
|||||||
token: string;
|
token: string;
|
||||||
repoContext: RepoContext;
|
repoContext: RepoContext;
|
||||||
}): Promise<RunContext> {
|
}): Promise<RunContext> {
|
||||||
const apiUrl = getApiUrl();
|
|
||||||
const timeoutMs = 30000;
|
const timeoutMs = 30000;
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await apiFetch({
|
||||||
`${apiUrl}/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
|
path: `/api/repo/${params.repoContext.owner}/${params.repoContext.name}/run-context`,
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${params.token}`,
|
Authorization: `Bearer ${params.token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...getVercelBypassHeaders(),
|
|
||||||
},
|
},
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
|||||||
+3
-18
@@ -3,7 +3,6 @@
|
|||||||
* Redacts actual secret values rather than using pattern matching
|
* Redacts actual secret values rather than using pattern matching
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { agentsManifest } from "../external.ts";
|
|
||||||
import { getGitHubInstallationToken } from "./token.ts";
|
import { getGitHubInstallationToken } from "./token.ts";
|
||||||
|
|
||||||
// patterns for sensitive env var names
|
// patterns for sensitive env var names
|
||||||
@@ -52,28 +51,14 @@ export function resolveEnv(mode: EnvMode | undefined): Record<string, string | u
|
|||||||
function getAllSecrets(): string[] {
|
function getAllSecrets(): string[] {
|
||||||
const secrets: string[] = [];
|
const secrets: string[] = [];
|
||||||
|
|
||||||
// get all API key values from agent manifest
|
// collect all env var values matching SENSITIVE_PATTERNS
|
||||||
for (const agent of Object.values(agentsManifest)) {
|
|
||||||
for (const keyName of agent.apiKeyNames) {
|
|
||||||
const envKey = keyName.toUpperCase();
|
|
||||||
const value = process.env[envKey];
|
|
||||||
if (value) {
|
|
||||||
secrets.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// for OpenCode: also scan all API_KEY environment variables (since apiKeyNames is empty)
|
|
||||||
const opencodeAgent = agentsManifest.opencode;
|
|
||||||
if (opencodeAgent && opencodeAgent.apiKeyNames.length === 0) {
|
|
||||||
for (const [key, value] of Object.entries(process.env)) {
|
for (const [key, value] of Object.entries(process.env)) {
|
||||||
if (value && typeof value === "string" && key.includes("API_KEY")) {
|
if (value && isSensitiveEnvName(key)) {
|
||||||
secrets.push(value);
|
secrets.push(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// add GitHub installation token
|
// add GitHub installation token (stored in memory, not in env)
|
||||||
try {
|
try {
|
||||||
const token = getGitHubInstallationToken();
|
const token = getGitHubInstallationToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
Reference in New Issue
Block a user