fix(cli): forward HEADERS env var on every backend fetch call (#9075)

Several `fetch()` callers in the CLI bypassed `OpenAPI.HEADERS` and skipped
the `HEADERS` env var, causing requests to fail behind auth gateways like
Cloudflare Access (same shape as #6421):

- `pushScript()` `/scripts/create` and `/scripts/create_snapshot` — regressed
  in #8936 when the call switched from `wmill.createScript()` (SDK) to a raw
  `fetch` for the `skip_if_noop` query param.
- Script preview `/jobs/run/preview_bundle`.
- App dev `/jobs_u/getupdate_sse` SSE stream.
- `wmill docs` `/api/inkeep`.

All four now spread `getHeaders()` and call `detectAuthGatewayChallenge()`
so a Cloudflare/SSO challenge surfaces a clear error instead of an opaque
JSON parse failure.

Adds `test/headers_env_var.test.ts`: spins up an auth-gateway proxy that
403s requests missing `CF-Access-Client-Id` / `CF-Access-Client-Secret` and
otherwise reverse-proxies to the test backend, then runs `wmill sync push`
of a fresh script through the proxy. Negative case (no `HEADERS` env)
verifies the proxy actually gates; positive case asserts every request
including `/scripts/create` reaches the backend with the headers attached.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-05-08 04:05:13 +00:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 0b0999ef93
commit d6476862b3
4 changed files with 287 additions and 4 deletions
+17 -3
View File
@@ -12,7 +12,8 @@ import * as log from "../../core/log.ts";
import { sep as SEP } from "node:path";
import * as path from "node:path";
import { stringify as yamlStringify } from "yaml";
import { deepEqual, readTextFile, readTextFileSync } from "../../utils/utils.ts";
import { deepEqual, getHeaders, readTextFile, readTextFileSync } from "../../utils/utils.ts";
import { detectAuthGatewayChallenge } from "../../utils/http_guards.ts";
import * as wmill from "../../../gen/services.gen.ts";
import * as specificItems from "../../core/specific_items.ts";
import { getCurrentGitBranch } from "../../utils/git.ts";
@@ -725,6 +726,7 @@ async function createScript(
// (same content, lockfile, and metadata) as a no-op, so the CLI does not
// produce phantom git-sync / promotion commits on re-pushes.
const skipIfNoop = "skip_if_noop=true";
const extraHeaders = getHeaders();
if (!bundleContent) {
try {
const url =
@@ -738,9 +740,11 @@ async function createScript(
headers: {
Authorization: `Bearer ${workspace.token}`,
"Content-Type": "application/json",
...extraHeaders,
},
body: JSON.stringify(body),
});
await detectAuthGatewayChallenge(req, url);
if (req.status != 201) {
throw Error(
`${req.status} - ${req.statusText} - ${await req.text()}`
@@ -771,9 +775,13 @@ async function createScript(
skipIfNoop;
const req = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${workspace.token} ` },
headers: {
Authorization: `Bearer ${workspace.token} `,
...extraHeaders,
},
body: form,
});
await detectAuthGatewayChallenge(req, url);
if (req.status != 201) {
throw Error(
`Script snapshot creation was not successful: ${req.status} - ${
@@ -1587,12 +1595,18 @@ async function preview(
workspace.workspaceId +
"/jobs/run/preview_bundle";
const extraHeaders = getHeaders();
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${workspace.token}` },
headers: {
Authorization: `Bearer ${workspace.token}`,
...extraHeaders,
},
body: form,
});
await detectAuthGatewayChallenge(response, url);
if (!response.ok) {
throw new Error(
`Preview failed: ${response.status} - ${response.statusText} - ${await response.text()}`