Skip to content

feat(mercury): egress_profile indirection for static-IP relay routing#249

Merged
chitcommit merged 3 commits into
mainfrom
mercury-egress-profile
Jun 16, 2026
Merged

feat(mercury): egress_profile indirection for static-IP relay routing#249
chitcommit merged 3 commits into
mainfrom
mercury-egress-profile

Conversation

@chitcommit

Copy link
Copy Markdown
Contributor

What

Introduces an egress_profile indirection for the Mercury banking proxy so calls can route through a static-IP relay without further code changes once the egress node exists. Mercury per-token IP allowlisting is why chicago / it-can-be get 401 from Cloudflare's shared egress; the fix is to send Mercury calls out through one static IP. This makes ChittyConnect ready for that relay — it does not build the egress node (blocked on key-management authority).

How

mercuryFetch now honors an active egress profile resolved per-request:

  • direct (default) — unchanged. Hits api.mercury.com directly with Authorization: Bearer, byte-for-byte the legacy request shape. Preserves today's working aribia path.
  • relay — POSTs to MERCURY_EGRESS_URL with the Mercury token as X-Mercury-Token plus CF-Access-Client-Id / CF-Access-Client-Secret service-token headers so the relay is Access-locked. The relay envelope carries { method, path, body } with a relative path only — api.mercury.com host-allowlisting is preserved structurally (caller can never redirect to another host). Token stays in Secrets (Option A), transits as a header, never stored on the relay.
  • Fail closed — if relay is selected but MERCURY_EGRESS_URL is unset, buildEgressRequest throws. No silent fallback to direct (that would mask misconfig and leak the shared egress IP).

Profile selection: global MERCURY_EGRESS_PROFILE (default direct) with optional per-slug override MERCURY_EGRESS_PROFILE_<SLUG>, mirroring getMercuryToken's existing env convention. No DB table — env suffices for 7 entities, structured so a mercury_connection { entity_id, egress_profile_id, allowed_hosts[], account_ids[] } record maps cleanly later.

Files

  • src/api/routes/thirdparty.jsresolveEgressProfile + pure buildEgressRequest extracted; mercuryFetch takes an egress arg; requireMercuryToken stashes mercuryEgress; 4 call sites updated (accounts, account, transactions, refresh).
  • tests/api/thirdparty-mercury-egress.test.js — 14 mock-free unit tests (no vi.mock): profile selection, per-slug override, header construction, fail-closed, direct-shape preservation, token-not-in-body. Relay HTTP round-trip deferred to an integration test once the egress node is provisioned (noted in-file).
  • wrangler.jsoncMERCURY_EGRESS_PROFILE/MERCURY_EGRESS_URL as non-secret vars (default direct/empty) across dev/staging/prod; MERCURY_EGRESS_ACCESS_CLIENT_ID/_SECRET as Secrets Store bindings mirroring the MERCURY_OIDC_* block.

Validation

  • npx vitest run tests/api/thirdparty-mercury-egress.test.js14 passed
  • Existing thirdparty-neon + thirdparty-notion-aliases6 passed (no regression)
  • eslint src/api/routes/thirdparty.js → clean
  • wrangler.jsonc → valid JSONC

The ONE config change to activate (once the egress node exists)

Set, per the credential hierarchy:

  • vars: MERCURY_EGRESS_PROFILE=relay, MERCURY_EGRESS_URL=https://<egress-node>/mercury
  • Secrets Store: MERCURY_EGRESS_ACCESS_CLIENT_ID, MERCURY_EGRESS_ACCESS_CLIENT_SECRET (Cloudflare Access service token for the relay)

No code change. direct remains the default until then.

Not in scope (blocked / out of lane)

Egress node provisioning and credential sweep/rotation — both require authority this workstream does not hold.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings June 12, 2026 06:09
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
❌ Deployment failed
View logs
chittyconnect 83ccb47 Jun 16 2026, 05:12 PM

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@chitcommit, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 24 minutes and 41 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b4d198e-e313-4203-b151-b6a8e9b65b9a

📥 Commits

Reviewing files that changed from the base of the PR and between c27eab2 and 83ccb47.

📒 Files selected for processing (3)
  • src/api/routes/thirdparty.js
  • tests/api/thirdparty-mercury-egress.test.js
  • wrangler.jsonc
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mercury-egress-profile

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Mercury per-token IP allowlisting requires all calls leave from one static
IP. This adds a configurable egress profile so Mercury proxy calls can route
through a relay (Access-locked, reserved-IP app-connector) without further
code changes once the egress node exists.

- `direct` (default): unchanged — hit api.mercury.com directly with Bearer
  auth, byte-for-byte the legacy request shape (preserves the working aribia
  path).
- `relay`: POST to MERCURY_EGRESS_URL with the Mercury token as
  X-Mercury-Token plus CF-Access-Client-Id/Secret service-token headers. The
  relay receives a relative `path` only (host allowlist preserved
  structurally). Fails closed if relay is selected but MERCURY_EGRESS_URL is
  unset (no silent fallback).

Profile is global (MERCURY_EGRESS_PROFILE) with optional per-slug override
(MERCURY_EGRESS_PROFILE_<SLUG>), mirroring getMercuryToken's convention. Keys
stay in Secrets (Option A) and transit as headers; never stored on the relay.

Pure buildEgressRequest/resolveEgressProfile extracted for mock-free unit
tests (14 tests). Relay HTTP round-trip deferred to integration test once the
egress node is provisioned (noted in test).

wrangler.jsonc: MERCURY_EGRESS_PROFILE/URL as non-secret vars (default
direct/empty); MERCURY_EGRESS_ACCESS_CLIENT_ID/SECRET as Secrets Store
bindings mirroring the MERCURY_OIDC_* block — across dev/staging/prod.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chitcommit chitcommit force-pushed the mercury-egress-profile branch from a401e1b to 036d89f Compare June 12, 2026 06:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an egress-profile indirection to the Mercury proxy so Mercury API calls can be routed either directly (current behavior) or via a future static-IP relay without further code changes, plus configuration scaffolding and unit tests.

Changes:

  • Adds resolveEgressProfile + buildEgressRequest and threads the resolved egress profile through Mercury route handlers.
  • Introduces a relay request envelope ({ method, path, body }) and Cloudflare Access service-token headers for relay authentication.
  • Adds unit tests for profile selection and request construction; updates wrangler.jsonc vars/secrets bindings for the new egress config.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/api/routes/thirdparty.js Adds egress profile resolution + request construction and wires Mercury handlers to use it.
tests/api/thirdparty-mercury-egress.test.js Adds unit tests covering egress profile selection and direct/relay request shapes.
wrangler.jsonc Adds egress vars defaults and Secrets Store bindings for relay Access credentials across envs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/api/routes/thirdparty.js
Comment thread src/api/routes/thirdparty.js Outdated
chitcommit and others added 2 commits June 12, 2026 06:16
….jsonc

The initial egress wiring inserted MERCURY_EGRESS_ACCESS_CLIENT_ID/SECRET and
MERCURY_EGRESS_PROFILE/URL twice per env block, which wrangler rejects
("assigned to multiple Secrets Store Secret bindings"). Each var/binding now
appears exactly once per dev/staging/production. Verified with
`CHITTYCONNECT_SAFE_DEPLOY=1 wrangler deploy --dry-run --env production` —
config parses and all four egress bindings resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…review)

Address two code-review findings on the egress_profile indirection:

1. Path-injection guard (relay mode): the relay re-hosts the forwarded
   `path` onto api.mercury.com, so an absolute URL, protocol-relative
   `//host`, embedded scheme, or `..` traversal could redirect the relay
   off Mercury and break the host-allowlist guarantee. buildEgressRequest
   now fails closed unless `path` is a relative API path beginning with a
   single `/`. Query strings remain legal.

2. Case-insensitive profile matching: resolveEgressProfile normalizes
   MERCURY_EGRESS_PROFILE / per-slug overrides with trim+lowercase, so
   `RELAY`/`Relay` map to `relay` instead of silently falling back to
   direct. Unknown values now throw early (fail closed) rather than
   masking misconfig. The requireMercuryToken middleware catches the
   throw and returns a diagnosable 503.

Adds real unit tests for both: absolute/protocol-relative/traversal/
non-string path rejection (and that legit query-string paths still pass),
case-insensitive profile resolution, and unknown-profile rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chitcommit

Copy link
Copy Markdown
Contributor Author

Note on MERCURY_EGRESS_URL: this value is operator-controlled (set via wrangler vars/secrets, never caller-supplied) and is intentionally not host-validated — acceptable under the operator-trust model, the same trust boundary as any other deployment config. The caller-supplied path is now validated (see resolved thread) since that is the only attacker-influenced input that reaches the relay envelope.

@chitcommit chitcommit added security-approved Security review approved docs-approved Documentation review approved access-reviewed Access control review approved labels Jun 16, 2026
@chitcommit chitcommit merged commit 8e401bf into main Jun 16, 2026
33 of 35 checks passed
@chitcommit chitcommit deleted the mercury-egress-profile branch June 16, 2026 20:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

access-reviewed Access control review approved docs-approved Documentation review approved security-approved Security review approved

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants