Skip to content
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versioning: strict [semver](https://semver.org/) — bundle schema changes
always bump at least minor; breaking schema changes bump major.

## [Unreleased]

### Added
- Honor `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` on login and submit via undici's `EnvHttpProxyAgent` (#83).
- Network errors name a closed failure class (`connection refused`, TLS / corporate CA, `proxy required`) without echoing `error.message`, headers, or body (#83).
- Document corporate proxy / CA setup and the submit visibility-probe captive-proxy edge (`docs/corporate-networks.md`, #83).

## [0.13.0] - 2026-08-14

### Changed
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ and what the provenance attestation actually proves.
- [docs/scan.md](docs/scan.md) — full `scan` command reference
- [docs/exit-codes.md](docs/exit-codes.md) — exit codes for CI and shell scripts
- [docs/login-submit.md](docs/login-submit.md) — `login`, `submit`, `logout`
- [docs/corporate-networks.md](docs/corporate-networks.md) — proxy env vars, `NODE_EXTRA_CA_CERTS`, captive-proxy visibility probe
- [docs/identity-selection-memory.md](docs/identity-selection-memory.md) — how `scan`/`submit` remember your per-repo identity selection
- [docs/private-label.md](docs/private-label.md) — the mandatory private label: what it is, why it travels outside the bundle
- [docs/schema.md](docs/schema.md) — every bundle field, explained
Expand Down
77 changes: 77 additions & 0 deletions docs/corporate-networks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Corporate networks

`login` and `submit` are the only commands that talk to the network.
`scan` never does, including behind a proxy.

If device-flow login fails at connect, you are usually missing one of
three things: the proxy env vars, the corporate CA, or both. The CLI
will name the failure class (`connection refused`, `could not verify TLS
certificate`, `proxy required`) without echoing headers, bodies, or
Node's error text — those can contain tokens.

## Proxy env vars

Node's built-in `fetch` ignores `HTTP_PROXY` / `HTTPS_PROXY`. This CLI
attaches undici's `EnvHttpProxyAgent` only when one of these is set:

- `HTTP_PROXY` / `http_proxy`
- `HTTPS_PROXY` / `https_proxy`

`NO_PROXY` / `no_proxy` do **not** attach the agent. They are read by
the agent after it exists: hosts that must bypass the proxy, typically
`localhost` and your internal git host.

Example:

```bash
export HTTPS_PROXY=http://proxy.corp.example:8080
export NO_PROXY=localhost,127.0.0.1,.corp.example
npx redential login
```

Leave the HTTP(S)_PROXY vars unset on a direct network — the client
then uses the same dispatcher-less `fetch` as a machine with no proxy.

A `407` HTTP response, or a CONNECT tunnel that is not 200 (undici
surfaces that as `UND_ERR_ABORTED`), prints `proxy required`. That
usually means the env var is missing, the URL is wrong, or the proxy
wants authentication the CLI does not prompt for — set the vars your
IT docs specify; do not paste a password into an issue. TLS through
the proxy failing (`UND_ERR_PRX_TLS`) uses the certificate message
below, not this one.

## Corporate CA (`NODE_EXTRA_CA_CERTS`)

TLS-intercepting proxies re-sign HTTPS with a company CA. Node does not
use the OS trust store the way a browser does. Point it at the PEM your
IT already installed:

```bash
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/corp-root.pem
npx redential login
```

The path is an example. Use whatever file your IT documents. Without it,
login fails with `could not verify TLS certificate (corporate proxy? see
docs/corporate-networks.md)` — not a Redential outage.

## `submit`'s visibility probe vs a captive proxy

Before upload, `submit` may HEAD the git remote — **only** for remotes
that look like github.com / gitlab.com / bitbucket.org, never an
arbitrary self-hosted URL, never with your credentials. A confirmed
`2xx`/`3xx` blocks submit (the repo answered as publicly reachable).
Anything else, including a network error, is fail-open.

A captive corporate proxy that answers **200 for every host** will make
that probe look like "public." The CLI will refuse to submit and tell
you to connect the GitHub App instead. If the repo is actually private,
the check was wrong — that is the proxy lying, not a leak: the probe is
unauthenticated HEAD, and nothing from your bundle has been sent yet.
Workarounds: add the git host to `NO_PROXY` so the probe reaches the
real origin, or report the false block if you are on a known-public
host that is in fact private.

`headRequest` stays fail-open (`null` on error). A proxy that **times
out** or **refuses** the probe does not block submit; you get scan's
existing public-host warning and may proceed.
2 changes: 1 addition & 1 deletion docs/exit-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ below (it's whatever the underlying error happened to say), but the shape
| `ScanError` | Invalid repo or git state, missing `--author` / `--yes` in non-interactive mode, validation failures (private label, `--since`, secrets in bundle), unsupported `explain` skill, malformed internal signature files. |
| `AuthError` | `submit` without a stored session, wrong site URL on stored credentials, login denied/expired/timed out. |
| `SubmitError` | Upload refused after the remote-visibility gate (confirmed-public repo). |
| `NetworkError` | Login or submit HTTP failures (unreachable host, non-JSON response, unexpected status) after retries where applicable. |
| `NetworkError` | Login or submit HTTP failures (unreachable host, TLS/proxy class, non-JSON response, unexpected status) after retries where applicable. See [corporate-networks.md](corporate-networks.md) for proxy and corporate-CA setup. |

Messages are sanitized user-facing strings only — never tokens or bundle
payloads.
Expand Down
14 changes: 11 additions & 3 deletions docs/login-submit.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ this gate is its real, definitive answer:
check must never be flakier than `scan`'s own warn-only heuristic: on an
inconclusive result, `submit` falls back to printing
`publicHostWarning`'s own (longer) message and proceeds.
- A captive corporate proxy that answers `200` for every host will make
this probe look public and block submit. That is a false block, not a
leak — see [corporate-networks.md](corporate-networks.md).

## Identity corroboration (submit-only)

Expand Down Expand Up @@ -458,9 +461,14 @@ the network, so the boundary is worth stating precisely:

Every command-level error is one of `ScanError` / `AuthError` /
`SubmitError` / `NetworkError` (`src/errors.ts`). `NetworkError` messages
are built only from the request's host and HTTP status — never from
response headers or body — so a failed request can never echo a bearer
token or bundle content into a printed error. EOF on any interactive
are built from the request's host, HTTP status, and a closed failure-class
phrase taken from `error.code` (or HTTP 407) — never from response
headers, body, or `error.message` — so a failed request can never echo a
bearer token or bundle content into a printed error. Connect failures
that used to collapse into `Could not reach <host>.` now name
`connection refused`, `could not verify TLS certificate` (see
[corporate-networks.md](corporate-networks.md)), or `proxy required`
when the code is one of those classes. EOF on any interactive
prompt (attestation, author selection, or `submit`'s upload confirmation)
aborts with a non-zero exit code rather than hanging or silently
proceeding, consistent with `scan`'s existing prompts.
6 changes: 4 additions & 2 deletions docs/privacy-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,12 @@ is the real, network-backed check described above — an anonymous HTTP

`login`/`submit` are the first commands with anything worth leaking through
an error message — a bearer token, or the full bundle. `src/http-client.ts`
builds every `NetworkError` from the request's host and HTTP status only,
never from response headers or body.
builds every `NetworkError` from the request's host, HTTP status, and a
closed failure-class phrase from `error.code` — never from response
headers, body, or `error.message`.

| Test | Proves |
|---|---|
| `test/privacy/submit-guardrail.test.ts` → "a failed upload's error message names the host and status, never the token or bundle" | A `500` from the submit endpoint produces a `NetworkError` whose message contains the status code but neither the stored access token nor any bundle field (`schema_version` as a proxy for "the whole bundle got interpolated in"). |
| `test/http-client.test.ts` → reach-error cases | Connect failures interpolate only the host plus a closed class phrase. A planted token in `error.message` or a `407` body never appears in `NetworkError.message`. |
| `test/login.test.ts` (all cases, implicit) | `login`'s errors (`AuthError` for denied/expired/timed-out) are static, fixed strings — never built from the device code or any server response field, so there's no path for the code to end up in an error either. |
16 changes: 13 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
},
"dependencies": {
"commander": "^12.1.0",
"typescript": "^5.6.0"
"typescript": "^5.6.0",
"undici": "6.28.0"
},
"devDependencies": {
"vitest": "^2.1.0"
Expand Down
3 changes: 2 additions & 1 deletion src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class SubmitError extends Error {}

/** A request to SITE_URL (or a remote host, for the visibility check)
* couldn't complete or came back with a non-2xx status. Message is built
* from the request's host and status only — never headers or body — so it
* from the request's host, HTTP status, and a closed failure-class phrase
* from `error.code` — never headers, body, or `error.message` — so it
* can never echo a bearer token or bundle content. */
export class NetworkError extends Error {}
Loading