Skip to content
Merged
11 changes: 11 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,14 @@ rustflags = ["-C", "link-arg=/STACK:8388608"]

[target.'cfg(all(windows, target_env = "gnu"))']
rustflags = ["-C", "link-args=-Wl,--stack,8388608"]

# Keep a developer's real `socket login` state out of every test run: the
# CLI reads the JS socket-cli's persisted config.json as a fallback token
# source (crates/socket-patch-core/src/utils/socket_cli_config.rs), so an
# ambient login would silently flip "no token → public proxy" tests onto
# the authenticated path. Test child processes inherit this from the test
# binary; the env scrub loops in tests deliberately skip this variable.
# To exercise the config layer, tests (and manual runs) override it with a
# falsy value, e.g. `SOCKET_NO_CONFIG=0`.
[env]
SOCKET_NO_CONFIG = "1"
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ in this file — see `.github/workflows/release.yml` (`version` job).

### Added

- **`socket login` now configures socket-patch.** The JS Socket CLI's
persisted config (`<data dir>/socket/settings/config.json`) is read —
never written — as a fallback layer below env vars for `apiToken`,
`defaultOrg`, and `apiBaseUrl`: precedence per key is CLI flag > env var
> socket-cli config > built-in default. Four `SOCKET_CLI_*` env names
are accepted as silent peer aliases (`SOCKET_CLI_API_TOKEN`,
`SOCKET_CLI_ORG_SLUG`, `SOCKET_CLI_API_BASE_URL`,
`SOCKET_CLI_NO_API_TOKEN`); the canonical `SOCKET_*` names win. Two new
env-only toggles: `SOCKET_NO_API_TOKEN` ignores ambient tokens (env +
config; an explicit `--api-token` still authenticates) and
`SOCKET_NO_CONFIG` disables the config layer. A corrupt config file
warns once on stderr and is ignored; `--json` stdout is unaffected. The
telemetry endpoint now resolves the API base through the same chain as
client construction, so a config-supplied `apiBaseUrl` applies to both.
Design notes: `docs/design/configuration.md`.


- **Hosted patch mode: `scan --mode hosted` (a.k.a. the hidden `--redirect`).**
The third patch-application mode: instead of applying in place (agent) or
committing artifacts (vendored), `scan` rewrites lockfiles / registry
Expand Down Expand Up @@ -272,6 +289,11 @@ in this file — see `.github/workflows/release.yml` (`version` job).

### Changed

- `--api-url` / `--proxy-url` no longer carry clap-level defaults: with
neither flag nor env var set they parse as unset and the documented
default URLs are applied at API-client construction (after the
socket-cli config layer). Observable behavior is unchanged unless a
socket-cli login exists.
- **All ecosystem feature flags removed — every ecosystem is always compiled
in.** The `cargo`, `golang`, `maven`, `composer`, `nuget`, and `deno` Cargo
features are gone from both crates; npm, PyPI, Ruby gems, Go, Cargo, NuGet,
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,24 @@ These flags are accepted by **every** subcommand — they are flattened into eac

Each flag has a matching `SOCKET_*` environment variable. **Precedence is CLI arg > env var > default**, so a flag on the command line always wins over the environment.

### Configuration sources

For the three authentication settings, the [Socket CLI](https://docs.socket.dev/docs/socket-cli)'s persisted login sits between the env var and the built-in default — run `socket login` (or `socket config set apiToken` / `defaultOrg`) once and socket-patch picks it up too. Resolution is per key, and an empty value means "unset" at every layer:

```
--api-token / --org / --api-url
1. CLI flag
2. Env var SOCKET_API_TOKEN / SOCKET_ORG_SLUG / SOCKET_API_URL
3. Peer alias env SOCKET_CLI_API_TOKEN / SOCKET_CLI_ORG_SLUG / SOCKET_CLI_API_BASE_URL
4. socket-cli config <data dir>/socket/settings/config.json — read-only
(Linux: ~/.local/share; macOS: ~/Library/Application Support
then legacy ~/.local/share, both after $XDG_DATA_HOME;
Windows: %LOCALAPPDATA%)
5. Built-in default no token → public proxy; org → auto-resolve; https://api.socket.dev
```

Two env-only toggles adjust this: `SOCKET_NO_API_TOKEN=1` ignores ambient tokens (env + config; an explicit `--api-token` still wins), and `SOCKET_NO_CONFIG=1` disables the config-file layer entirely. socket-patch never *writes* the config file, and a corrupt one only produces a stderr warning — it never breaks a command or pollutes `--json` output. socket-patch does **not** read `.env` files or any per-repository config for endpoints or credentials: a cloned repo must never be able to redirect where patches come from or spend your token.

| Flag | Env var | Description |
|------|---------|-------------|
| `--cwd <dir>` | `SOCKET_CWD` | Working directory (default: `.`). The manifest path is resolved relative to this. |
Expand Down
42 changes: 41 additions & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This document defines the **public surface** of the `socket-patch` binary. Anyth

## Global arguments

In v3.0 every subcommand accepts the same set of "global" flags via a single shared `GlobalArgs` struct that's `#[command(flatten)]`-ed into each per-command struct (`crates/socket-patch-cli/src/args.rs`). Subcommands that don't actually consume a given flag accept it silently — e.g. `list --global` parses fine and is a no-op. Every flag also has an environment-variable binding; precedence is **CLI arg > env var > default**.
In v3.0 every subcommand accepts the same set of "global" flags via a single shared `GlobalArgs` struct that's `#[command(flatten)]`-ed into each per-command struct (`crates/socket-patch-cli/src/args.rs`). Subcommands that don't actually consume a given flag accept it silently — e.g. `list --global` parses fine and is a no-op. Every flag also has an environment-variable binding; precedence is **CLI arg > env var > default** — and for exactly three keys (`--api-token`, `--org`, `--api-url`) the JS socket-cli's persisted login sits between env var and default: **CLI arg > env var (canonical, then `SOCKET_CLI_*` alias) > socket-cli `config.json` > default**. See "Persisted configuration" under Environment variables.

| Long | Short | Env var | Default | Type | Semantic |
|---|---|---|---|---|---|
Expand Down Expand Up @@ -598,6 +598,10 @@ worse, lets a warm cache silently serve unpatched bytes):

All v3.0 env vars use the `SOCKET_*` prefix. Three legacy `SOCKET_PATCH_*` names are still honored at runtime for compatibility: on first read of any of the three the binary emits a one-shot deprecation warning to stderr (the warning fires unconditionally — even under `--silent` / `--json` — because it's a transition signal users need to see). The legacy names will be removed in the next major release.

Four `SOCKET_CLI_*` names from the sibling JS Socket CLI are additionally accepted as **peer aliases** (supported, not deprecated — no warning): `SOCKET_CLI_API_TOKEN` → `SOCKET_API_TOKEN`, `SOCKET_CLI_ORG_SLUG` → `SOCKET_ORG_SLUG`, `SOCKET_CLI_API_BASE_URL` → `SOCKET_API_URL`, `SOCKET_CLI_NO_API_TOKEN` → `SOCKET_NO_API_TOKEN`. The canonical `SOCKET_*` name always wins when both are set; promotion is silent and happens in-process before clap parses. Other socket-cli names (`SOCKET_CLI_CONFIG`, `SOCKET_CLI_API_PROXY`, `SOCKET_CLI_DEBUG`) are deliberately **not** honored.

Empty string means unset at every layer: exported-but-empty flag-bound vars are scrubbed before clap parses, and the API-client resolution filters empty values at each fallback step.

| Env var | CLI equivalent | Default | Notes |
|---|---|---|---|
| `SOCKET_CWD` | `--cwd` | `.` | — |
Expand Down Expand Up @@ -634,6 +638,42 @@ All v3.0 env vars use the `SOCKET_*` prefix. Three legacy `SOCKET_PATCH_*` names
| `SOCKET_VEX` | `apply --vex` / `scan --vex` / `vendor --vex` | (none) | Embedded OpenVEX output path. The `SOCKET_VEX_*` knobs (`_PRODUCT`, `_NO_VERIFY`, `_DOC_ID`, `_COMPACT`) are shared with the standalone `vex` command; on the host commands they bind to `--vex-product` etc. |
| `SOCKET_VEX_OUTPUT` | `vex --output` / `-O` | (none) | Local to the standalone `vex`: document output path (required with `--json`). |

### Config-layer toggles (env-only)

| Env var | Default | Notes |
|---|---|---|
| `SOCKET_NO_CONFIG` | `false` | Truthy (`1`/`true`/`yes`/`on`): disable the socket-cli persisted-config fallback layer entirely — pure flag+env behavior. Also the test-hermeticity switch (the workspace `.cargo/config.toml` exports it as `1` for every cargo-run process). |
| `SOCKET_NO_API_TOKEN` | `false` | Truthy: ignore **ambient** API tokens (the `SOCKET_API_TOKEN` env var and the socket-cli config token); only an explicit `--api-token` flag authenticates. Peer alias: `SOCKET_CLI_NO_API_TOKEN`. |

### Persisted configuration (socket-cli `config.json`)

The binary reads — **never writes** — the JS Socket CLI's persisted config, so a single `socket login` (or `socket config set apiToken/defaultOrg`) configures socket-patch too. The file is `<data dir>/socket/settings/config.json`, a base64-encoded JSON object:

| Platform | Location |
|---|---|
| Linux | `$XDG_DATA_HOME` or `~/.local/share`, + `/socket/settings/config.json` |
| macOS | `$XDG_DATA_HOME` or `~/Library/Application Support`, + `/socket/settings/config.json`; when `$XDG_DATA_HOME` is unset the legacy `~/.local/share` location is probed second (older socket-cli releases wrote the Linux-style path on every platform) |
| Windows | `%LOCALAPPDATA%` or `%USERPROFILE%\AppData\Local`, + `\socket\settings\config.json` |

Exactly three keys are honored, each slotting **below** the env var and **above** the built-in default for its setting, resolved per key independently:

| Config key | Feeds | Env var above it |
|---|---|---|
| `apiToken` | `--api-token` | `SOCKET_API_TOKEN` |
| `defaultOrg` (alias `org`; `defaultOrg` wins) | `--org` | `SOCKET_ORG_SLUG` |
| `apiBaseUrl` | `--api-url` | `SOCKET_API_URL` |

Contract properties:

- **Read-only pledge**: socket-patch never creates, modifies, or deletes this file; socket-cli owns it. There is no `socket-patch login`/`config` subcommand — use `socket login`.
- Other socket-cli keys (`apiProxy`, `enforcedOrgs`, `skipAskToPersistDefaultOrg`) and unknown keys are ignored. Non-string or empty values for the three honored keys count as unset. For an HTTP forward proxy use the standard `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` vars, which the HTTP client honors; socket-cli's `apiProxy` is deliberately not mapped (and is unrelated to `--proxy-url`, which is the public patch *endpoint*).
- Missing file / unresolvable data dir: silent (the normal case). Present but unreadable or undecodable (not base64(JSON), with a plain-JSON leniency fallback): a one-shot stderr warning naming the path, then treated as absent — never fatal, and `--json` stdout stays clean (all diagnostics are stderr-only).
- The file is read lazily at most once per process, only when a key is still unresolved after flag + env.
- The telemetry endpoint resolver shares the same `apiBaseUrl` chain as API-client construction (`resolve_api_base_url`), so telemetry can never target a different host than the client.
- `--offline` semantics are unchanged: reading the local file is not network contact; a config-sourced token is inert offline.
- **Repo-level files never carry endpoints, credentials, or interlock-disablers**: configuration for those comes only from flags, env vars, this user-level file, and built-in defaults — never from files inside the repository being patched (manifest, socket.yml, `.env`, …).
- `--debug` names the source on stderr whenever a setting resolves from the socket-cli config (the token value itself is never echoed).

### Registry override env vars

Env-only knobs (no CLI flag) read by the vendor auto-fetch / artifact-rebuild paths in `socket-patch-core` (`src/patch/vendor/registry_fetch.rs`, `src/patch/vendor/maven_repo.rs`). Each is the enterprise-mirror / test escape hatch for one registry base; trailing slashes are trimmed and an exported-but-empty value falls back to the default. Lock-recorded URLs (npm/yarn/composer/gem/uv `resolved`/dist URLs) are used verbatim and bypass these.
Expand Down
76 changes: 34 additions & 42 deletions crates/socket-patch-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ use std::path::{Path, PathBuf};
use clap::Args;

use socket_patch_core::api::client::ApiClientEnvOverrides;
use socket_patch_core::constants::{
DEFAULT_PATCH_API_PROXY_URL, DEFAULT_PATCH_MANIFEST_PATH, DEFAULT_SOCKET_API_URL,
};
use socket_patch_core::constants::DEFAULT_PATCH_MANIFEST_PATH;
use socket_patch_core::crawlers::Ecosystem;
use socket_patch_core::patch::vendor::VendorSource;

Expand Down Expand Up @@ -97,13 +95,12 @@ pub struct GlobalArgs {
)]
pub manifest_path: String,

/// Socket API URL (authenticated endpoint).
#[arg(
long = "api-url",
env = "SOCKET_API_URL",
default_value = DEFAULT_SOCKET_API_URL,
)]
pub api_url: String,
/// Socket API URL (authenticated endpoint) [default:
/// https://api.socket.dev]. No clap default: `None` lets the core
/// resolver fall through env and the socket-cli config file before
/// applying `DEFAULT_SOCKET_API_URL`.
#[arg(long = "api-url", env = "SOCKET_API_URL")]
pub api_url: Option<String>,

/// Socket API token. Absence selects the public patch proxy.
#[arg(long = "api-token", env = "SOCKET_API_TOKEN")]
Expand All @@ -113,13 +110,11 @@ pub struct GlobalArgs {
#[arg(long = "org", short = 'o', env = "SOCKET_ORG_SLUG")]
pub org: Option<String>,

/// Public proxy URL used when no API token is set.
#[arg(
long = "proxy-url",
env = "SOCKET_PROXY_URL",
default_value = DEFAULT_PATCH_API_PROXY_URL,
)]
pub proxy_url: String,
/// Public proxy URL used when no API token is set [default:
/// https://patches-api.socket.dev]. No clap default, matching
/// `api_url` — resolution happens in `get_api_client_with_overrides`.
#[arg(long = "proxy-url", env = "SOCKET_PROXY_URL")]
pub proxy_url: Option<String>,

/// Restrict to these ecosystems (comma-separated). Names that are not
/// supported ecosystems are rejected.
Expand Down Expand Up @@ -299,20 +294,18 @@ impl GlobalArgs {

/// Build [`ApiClientEnvOverrides`] from the CLI flags.
///
/// `api_token` and `org` are forwarded as `Some(_)` only when set.
/// `api_url` and `proxy_url` are forwarded only when non-empty;
/// `GlobalArgs::default()` leaves both empty so integration tests
/// that mutate env vars *after* constructing args still get env-var
/// resolution from `get_api_client_with_overrides`. In production
/// clap always populates them with either the CLI value, the env
/// value, or the clap-declared default — all non-empty — so the
/// resolved value still flows through.
/// Every field is forwarded as `Some(_)` only when set and non-empty.
/// `None` (no flag, no env var — the fields carry no clap default)
/// defers resolution to `get_api_client_with_overrides`, which falls
/// through env vars and the socket-cli config file to the built-in
/// defaults. The empty filter keeps `--api-url ""` meaning "unset"
/// rather than forwarding a blank override.
pub fn api_client_overrides(&self) -> ApiClientEnvOverrides {
ApiClientEnvOverrides {
api_url: Some(self.api_url.clone()).filter(|s| !s.is_empty()),
api_url: self.api_url.clone().filter(|s| !s.is_empty()),
api_token: self.api_token.clone().filter(|s| !s.is_empty()),
org_slug: self.org.clone().filter(|s| !s.is_empty()),
proxy_url: Some(self.proxy_url.clone()).filter(|s| !s.is_empty()),
proxy_url: self.proxy_url.clone().filter(|s| !s.is_empty()),
}
}
}
Expand Down Expand Up @@ -425,21 +418,20 @@ impl Default for GlobalArgs {
/// when neither CLI flag nor env var is set), so this `Default` is
/// only reached from tests building `GlobalArgs` directly.
///
/// `api_url` and `proxy_url` are intentionally **empty** here (not
/// the production default URLs). That lets tests set
/// `SOCKET_API_URL` / `SOCKET_PROXY_URL` via `std::env::set_var`
/// *after* constructing the args struct and have those env vars
/// flow through to the API client — `api_client_overrides` skips
/// empty values so the underlying `get_api_client_with_overrides`
/// falls back to env-var resolution.
/// `api_url` and `proxy_url` are `None` here (not the production
/// default URLs). That lets tests set `SOCKET_API_URL` /
/// `SOCKET_PROXY_URL` via `std::env::set_var` *after* constructing
/// the args struct and have those env vars flow through to the API
/// client — `api_client_overrides` forwards `None` so the underlying
/// `get_api_client_with_overrides` falls back to env-var resolution.
fn default() -> Self {
Self {
cwd: PathBuf::from("."),
manifest_path: DEFAULT_PATCH_MANIFEST_PATH.to_string(),
api_url: String::new(),
api_url: None,
api_token: None,
org: None,
proxy_url: String::new(),
proxy_url: None,
ecosystems: None,
download_mode: "diff".to_string(),
vendor_source: "auto".to_string(),
Expand Down Expand Up @@ -817,10 +809,10 @@ mod tests {
#[test]
fn api_client_overrides_forwards_set_values() {
let args = GlobalArgs {
api_url: "https://api.example.com".to_string(),
api_url: Some("https://api.example.com".to_string()),
api_token: Some("tok123".to_string()),
org: Some("acme".to_string()),
proxy_url: "https://proxy.example.com".to_string(),
proxy_url: Some("https://proxy.example.com".to_string()),
..GlobalArgs::default()
};
let o = args.api_client_overrides();
Expand All @@ -830,8 +822,8 @@ mod tests {
assert_eq!(o.proxy_url.as_deref(), Some("https://proxy.example.com"));
}

/// `GlobalArgs::default()` leaves `api_url`/`proxy_url` empty and the
/// optional fields `None`, so every override must come back `None` —
/// `GlobalArgs::default()` leaves every field `None`,
/// so every override must come back `None` —
/// this is what lets integration tests set `SOCKET_*` env vars *after*
/// constructing args and still have env-var resolution win downstream.
#[test]
Expand All @@ -851,10 +843,10 @@ mod tests {
#[test]
fn api_client_overrides_filters_empty_strings() {
let args = GlobalArgs {
api_url: String::new(),
api_url: Some(String::new()),
api_token: Some(String::new()),
org: Some(String::new()),
proxy_url: String::new(),
proxy_url: Some(String::new()),
..GlobalArgs::default()
};
let o = args.api_client_overrides();
Expand Down
8 changes: 4 additions & 4 deletions crates/socket-patch-cli/src/commands/repair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -469,10 +469,10 @@ async fn repair_inner(
if !args.common.json {
eprintln!("Warning: {label} cleanup failed: {e}");
}
env.record(PatchEvent::artifact(PatchAction::Skipped).with_reason(
"cleanup_failed",
format!("{label} cleanup failed: {e}"),
));
env.record(
PatchEvent::artifact(PatchAction::Skipped)
.with_reason("cleanup_failed", format!("{label} cleanup failed: {e}")),
);
}
}
}
Expand Down
3 changes: 1 addition & 2 deletions crates/socket-patch-cli/src/commands/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,7 @@ pub async fn run(args: VendorArgs) -> i32 {
}
} else {
let params = args.vex.to_build_params();
match generate_vex_from_manifest_path(&args.common, &params, &manifest_path).await
{
match generate_vex_from_manifest_path(&args.common, &params, &manifest_path).await {
Ok(summary) => {
env.vex = Some(VexSummary {
path: vex_path.display().to_string(),
Expand Down
Loading
Loading