Skip to content

feat: proxy mode (6.1.0-beta.1) - #20

Open
tomplex wants to merge 5 commits into
masterfrom
proxy-mode
Open

feat: proxy mode (6.1.0-beta.1)#20
tomplex wants to merge 5 commits into
masterfrom
proxy-mode

Conversation

@tomplex

@tomplex tomplex commented May 13, 2026

Copy link
Copy Markdown

Summary

Adds a "proxy mode" so consumers can talk to an in-pod Vault Agent's API-proxy listener instead of reading a token file at startup. Released as 6.1.0-beta.1 — a new minor so the existing ^6.0.0-beta.1 ranges in downstream package.jsons don't silently absorb it; each consumer's upgrade is an explicit lockfile change.

Why

In our k8s deployments, services that import vault-env race the vault2-agent sidecar at startup — vault-env reads /vault2/secrets/vault-token exactly once on import, and on a cold pod that file doesn't exist yet, so the process throws Expected VAULT_TOKEN to be set and crash-loops until the agent wins. We've been eating 2–3 restarts per deploy because of this. The fix is to stop reading the file at all: point VAULT_ADDR at the Vault Agent's local API-proxy listener (http://127.0.0.1:8201), which we configure with use_auto_auth_token = "force" — the agent injects its auto-auth token on every request, so clients send no X-Vault-Token header. As a bonus, that removes a separate class of bug where the file-read token would silently expire after its TTL.

What this PR does

When VAULT_TOKEN is unset:

  • Default VAULT_ADDR to http://127.0.0.1:8201 (was :8200, which was never the agent port — it was always the wrong default).
  • Skip the /vault2/secrets/vault-token file read entirely (also drops the previous "retry 5 times" loop, which was a no-op anyway — setTimeout doesn't block, so the while spun to exhaustion synchronously).
  • Don't throw Expected VAULT_TOKEN to be set.
  • Send no X-Vault-Token header on requests (build the headers object conditionally — undefined would otherwise throw ERR_HTTP_INVALID_HEADER_VALUE).

When VAULT_TOKEN is set, behavior is unchanged — the token is used, VAULT_ADDR defaults to :8200 as before, and the header is sent. So existing consumers passing tokens explicitly (CI tests, anything outside k8s) are unaffected.

Plus: the initial synchronous secret fetch now retries with exponential backoff on connection errors and HTTP 5xx (≤ 6 attempts, 0/0.5/1/2/4/8s). 4xx fails fast as before — RetryAuthFailure (403) remains a hard stop. Implemented via Atomics.wait on a throwaway SharedArrayBuffer (the only clean way to block synchronously in Node without a busy-loop); needed lib: ["ES2017", "DOM", "ScriptHost"] in tsconfig.json since target: es5's default lib lacks Atomics/SharedArrayBuffer.

Commits

  1. test(vault-env): add proxy-mode test (no token, no header, no throw)
  2. feat(vault-env): proxy mode — no token => :8201, no X-Vault-Token, no throw
  3. test(vault-env): cover initial-fetch retry on transient failure
  4. feat(vault-env): retry the initial synchronous secret fetch on transient failures
  5. chore(vault-env): 6.1.0-beta.1

Testing

All 16 tests pass (2 new + 14 existing), tsc clean, eslint clean (6 pre-existing .d.ts warnings, unrelated). The fake-server fixture (test/fakeVault.ts + test/fakeVaultWorker.ts) gained two options:

  • acceptAnyToken — skip the token check and assert req.headers["x-vault-token"] === undefined. Used by the proxy-mode test to prove no header is sent.
  • failFirstRequests: N — reply 503 to the first N requests, then normal. Used by the retry test (failFirstRequests: 2prepare() must retry through both before succeeding).

Heads up — one thing to weigh in on

The test script in package.json got NODE_OPTIONS="--no-experimental-strip-types" prepended. On Node 25 (and 22.6+ with --experimental-strip-types defaulting on), mocha -r ts-node/register hits ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX on import assert = require("assert") — Node's built-in TS handler intercepts before ts-node does. The flag fixes it locally. But: that flag was only added in Node ~22.6; on older Node it errors bad option, breaking npm test. Options if that's a concern:

  • Keep as-is and add "engines": { "node": ">=22.6" }.
  • Drop it from the committed package.json and have devs/CI pass it inline when on Node ≥22.6.
  • Migrate off import x = require(...) syntax so Node's strip-types handler accepts it (a larger change).

I lean toward (1) if no one's still running this on Node 14, but happy to do (2) if you'd rather not constrain.

Downstream

The matching consumer migration in fdy is staged on a branch (tc/vault-token-startup-race); it bumps sojourner/zebra/kopeng/auth0/acceptance/magic/praxis to ^6.1.0-beta.1 and lands the matching ci-deploy-gcp + agent-config changes. It's gated on this PR merging and 6.1.0-beta.1 being published to npm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant