Skip to content

feat: add Mercury banking API proxy routes#130

Closed
chitcommit wants to merge 1 commit intomainfrom
feat/mercury-proxy-routes
Closed

feat: add Mercury banking API proxy routes#130
chitcommit wants to merge 1 commit intomainfrom
feat/mercury-proxy-routes

Conversation

@chitcommit
Copy link
Copy Markdown
Contributor

@chitcommit chitcommit commented Mar 28, 2026

Summary

  • Add Mercury banking API proxy routes to thirdparty.js
  • Routes: /api/thirdparty/mercury/accounts, /account/:id, /account/:id/transactions, /refresh
  • Per-login API key resolution via slug param (9 Mercury business accounts)
  • Legacy alias at /api/mercury/* for ChittyFinance compatibility

Test plan

  • Deploy to production
  • Add Mercury API keys as secrets
  • Test end-to-end

Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Integrated Mercury Banking API for account management.
    • New capabilities to view and manage connected accounts.
    • Added transaction history retrieval.
    • Added account refresh functionality for real-time data updates.

Multi-account Mercury support via /api/thirdparty/mercury/*:
- GET /accounts — list bank accounts for a Mercury login (by slug)
- GET /account/:id — single account details
- GET /account/:id/transactions — transactions with date/pagination
- POST /refresh — keepalive ping to prevent 30-day token expiry

Token resolution: header > env MERCURY_API_KEY_{SLUG} > MERCURY_API_TOKEN > 1Password broker

Legacy /api/mercury/* alias rewrites to /api/thirdparty/mercury/*
for ChittyFinance backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 28, 2026 04:27
@cloudflare-workers-and-pages
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 successful!
View logs
chittyconnect 4fe3bb0 Mar 28 2026, 04:24 AM

@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.

@cloudflare-workers-and-pages
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 successful!
View logs
chittyconnect 4fe3bb0 Mar 28 2026, 04:24 AM

2 similar comments
@cloudflare-workers-and-pages
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 successful!
View logs
chittyconnect 4fe3bb0 Mar 28 2026, 04:24 AM

@cloudflare-workers-and-pages
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 successful!
View logs
chittyconnect 4fe3bb0 Mar 28 2026, 04:24 AM

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

This PR adds Mercury Banking API proxy functionality by implementing a catch-all route that rewrites legacy /api/mercury/* requests to /api/thirdparty/mercury/*, along with new Mercury Banking API endpoints, token resolution helpers, and authenticated request handlers.

Changes

Cohort / File(s) Summary
Route Registration
src/api/router.js
Added catch-all route that rewrites /api/mercury/* requests to /api/thirdparty/mercury/* and forwards them to thirdparty routes handler.
Mercury Banking API Integration
src/api/routes/thirdparty.js
Implemented Mercury API proxy with authentication helpers (getMercuryToken, mercuryFetch) and four endpoints: list accounts, get account details, list account transactions, and perform keepalive refresh. All handlers include try/catch error handling with 503 for missing tokens and 500 for failures.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Router as api.all(/api/mercury/*)
    participant Handler as thirdpartyRoutes<br/>Handler
    participant getToken as getMercuryToken()
    participant Broker as Credential<br/>Broker
    participant Mercury as Mercury API<br/>api.mercury.com

    Client->>Router: GET /api/mercury/accounts
    Router->>Router: Rewrite to /api/thirdparty/mercury/accounts
    Router->>Handler: Forward request to thirdpartyRoutes.fetch()
    Handler->>getToken: Resolve Mercury Bearer Token
    alt Token in Header
        getToken->>Handler: Return X-Mercury-Token
    else Token in Env
        getToken->>Handler: Return MERCURY_API_KEY_{SLUG} or MERCURY_API_TOKEN
    else Token from Broker
        getToken->>Broker: Dynamically import & fetch token
        Broker->>getToken: Return credential
        getToken->>Handler: Return token
    else Missing
        Handler->>Client: 503 Service Unavailable
    end
    Handler->>Mercury: POST /api/v1/accounts (authenticated)
    Mercury->>Handler: Return account data
    Handler->>Client: 200 OK with response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • chittyos/chittyconnect#129: Directly related; both PRs add identical Mercury Banking API proxy implementation with the same catch-all legacy route rewriting and thirdparty endpoint handlers.

Poem

🐰 A Mercury proxy hops into view,
Routes rewritten, tokens shiny and new,
From /api/mercury/* to paths pristine,
Banking API calls flow clean as a dream,
Legacy bridges meet modern design—
Hop, hop, fetch! All systems align! 💫

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete. While it provides a summary of changes and test plan, it is missing most required sections from the template (Security & Access, Docs, Validation with CI status). Complete the description template by adding sections for Security & Access (with checkboxes), Docs (with checkboxes), and Validation (confirming CI is green with governance checks, lint, and tests passing).
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding Mercury banking API proxy routes, matching the primary functionality introduced.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mercury-proxy-routes

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/api/routes/thirdparty.js (2)

870-872: Code organization: Mercury routes added after the export statement.

While functionally correct (routes are registered on the already-exported thirdpartyRoutes object), placing route definitions after the export statement is unconventional and may confuse readers. Consider moving the export to the end of the file.

📦 Suggested reorganization

Move lines 871-1009 (Mercury routes) above the export statement at line 870, or move the export to the end of the file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/routes/thirdparty.js` around lines 870 - 872, The export of
thirdpartyRoutes occurs before the Mercury Banking API route definitions, which
is unconventional and confusing; move the Mercury route block (the routes
registering with thirdpartyRoutes for the Mercury Banking API) so that all route
registrations appear before the export, or alternatively relocate the export
statement (export { thirdpartyRoutes };) to the end of the file after the
Mercury routes, ensuring thirdpartyRoutes is fully defined prior to exporting.

906-920: Missing timeout on Mercury API calls.

Unlike the Ollama integration (lines 340-341), mercuryFetch has no timeout. If Mercury's API becomes unresponsive, requests will hang until the Worker runtime limit.

⏱️ Proposed fix: Add timeout with AbortController
-async function mercuryFetch(token, path, options = {}) {
-  const res = await fetch(`${MERCURY_API}${path}`, {
+async function mercuryFetch(token, path, options = {}, timeoutMs = 15000) {
+  const controller = new AbortController();
+  const timeout = setTimeout(() => controller.abort(), timeoutMs);
+  
+  const res = await fetch(`${MERCURY_API}${path}`, {
     ...options,
     headers: {
       "Content-Type": "application/json",
       Authorization: `Bearer ${token}`,
       ...options.headers,
     },
+    signal: controller.signal,
-  });
+  }).finally(() => clearTimeout(timeout));
+  
   if (!res.ok) {
     const body = await res.text().catch(() => "");
     throw new Error(`Mercury API ${res.status}: ${body.slice(0, 200)}`);
   }
   return res.json();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/routes/thirdparty.js` around lines 906 - 920, The mercuryFetch
function lacks a request timeout and can hang; add an AbortController inside
mercuryFetch, pass its signal to fetch (via options.signal), create a setTimeout
that calls controller.abort() after a configurable timeout (e.g., same timeout
used by Ollama), clear the timer when fetch completes, and when aborting throw
or rethrow a clear timeout error (e.g., "Mercury API timeout") so callers get a
deterministic failure instead of waiting for the Worker runtime to kill the
request.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/router.js`:
- Around line 159-164: The legacy Mercury handler uses api.all with
thirdpartyRoutes.fetch which bypasses the parent authenticate middleware (api
with authenticate registered), so replace this direct fetch to ensure middleware
runs: either rewrite the request and perform a 307 redirect via
c.redirect(url.toString(), 307) so the request re-enters the router chain, or
mount the legacy path using the standard router pattern
(api.route("/api/mercury", thirdpartyRoutes)) so thirdpartyRoutes inherits the
authenticate middleware; update the handler that references api.all and
thirdpartyRoutes.fetch accordingly to use one of these two approaches.

In `@src/api/routes/thirdparty.js`:
- Around line 1008-1009: The comment block saying "Legacy aliases for
ChittyFinance" is orphaned and misleading; either delete that comment from
src/api/routes/thirdparty.js or replace it with a short pointer stating that
legacy aliasing is handled in router.js via the catch-all rewrite (which maps
/api/mercury/accounts → /api/thirdparty/mercury/accounts). Update the comment to
mention router.js and the catch-all rewrite (or remove it entirely) so readers
looking at thirdparty.js are not misled about where aliases are implemented.
- Around line 896-904: The dynamic import of getCredential inside the credential
broker block is redundant because getCredential is already statically imported;
remove the await import("../../lib/credential-helper.js") line and the const {
getCredential } = ... declaration, and call the existing getCredential function
directly in the return statement (keep the same arguments: c.env,
`integrations/mercury/${integrationSlug || "default"}`, "MERCURY_API_TOKEN",
"Mercury") so there are no duplicate declarations or unnecessary runtime
imports.

---

Nitpick comments:
In `@src/api/routes/thirdparty.js`:
- Around line 870-872: The export of thirdpartyRoutes occurs before the Mercury
Banking API route definitions, which is unconventional and confusing; move the
Mercury route block (the routes registering with thirdpartyRoutes for the
Mercury Banking API) so that all route registrations appear before the export,
or alternatively relocate the export statement (export { thirdpartyRoutes };) to
the end of the file after the Mercury routes, ensuring thirdpartyRoutes is fully
defined prior to exporting.
- Around line 906-920: The mercuryFetch function lacks a request timeout and can
hang; add an AbortController inside mercuryFetch, pass its signal to fetch (via
options.signal), create a setTimeout that calls controller.abort() after a
configurable timeout (e.g., same timeout used by Ollama), clear the timer when
fetch completes, and when aborting throw or rethrow a clear timeout error (e.g.,
"Mercury API timeout") so callers get a deterministic failure instead of waiting
for the Worker runtime to kill the request.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cd1c3a61-7298-4343-b3b2-f5a0a7049cd9

📥 Commits

Reviewing files that changed from the base of the PR and between 52b9f13 and 4fe3bb0.

📒 Files selected for processing (2)
  • src/api/router.js
  • src/api/routes/thirdparty.js

Comment on lines +159 to +164
api.all("/api/mercury/*", async (c) => {
const url = new URL(c.req.url);
url.pathname = url.pathname.replace("/api/mercury", "/api/thirdparty/mercury");
const newReq = new Request(url.toString(), c.req.raw);
return thirdpartyRoutes.fetch(newReq, c.env, c.executionCtx);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that thirdpartyRoutes has no middleware and other routes use .route() pattern
echo "=== Checking thirdpartyRoutes middleware usage ==="
rg -n '\.use\(' src/api/routes/thirdparty.js | head -20

echo ""
echo "=== Checking how other routes are mounted (should use .route()) ==="
rg -n '\.fetch\(' src/api/router.js
rg -n '\.route\(' src/api/router.js | head -20

Repository: chittyos/chittyconnect

Length of output: 1316


Critical: Authentication bypass in Mercury legacy route handler.

The route at line 159-164 uses thirdpartyRoutes.fetch() which bypasses the authenticate middleware registered on the parent api router (line 88). All other routes in this file use api.route() pattern and properly inherit parent middleware. Mercury banking endpoints become accessible without authentication.

Use one of these approaches:

  1. Rewrite the URL and redirect through the normal middleware chain: c.redirect(url.toString(), 307)
  2. Mount at the legacy path using the standard pattern: api.route("/api/mercury", thirdpartyRoutes)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/router.js` around lines 159 - 164, The legacy Mercury handler uses
api.all with thirdpartyRoutes.fetch which bypasses the parent authenticate
middleware (api with authenticate registered), so replace this direct fetch to
ensure middleware runs: either rewrite the request and perform a 307 redirect
via c.redirect(url.toString(), 307) so the request re-enters the router chain,
or mount the legacy path using the standard router pattern
(api.route("/api/mercury", thirdpartyRoutes)) so thirdpartyRoutes inherits the
authenticate middleware; update the handler that references api.all and
thirdpartyRoutes.fetch accordingly to use one of these two approaches.

Comment on lines +896 to +904
// 4. Credential broker (1Password)
const { getCredential } = await import("../../lib/credential-helper.js");
return getCredential(
c.env,
`integrations/mercury/${integrationSlug || "default"}`,
"MERCURY_API_TOKEN",
"Mercury",
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify getCredential function signature
echo "=== getCredential function definition ==="
ast-grep --pattern 'async function getCredential($$$) { $$$ }'

echo ""
echo "=== All getCredential calls to check argument count ==="
rg -n 'getCredential\(' src/

Repository: chittyos/chittyconnect

Length of output: 3632


🏁 Script executed:

#!/bin/bash
# Check top-level imports in thirdparty.js
echo "=== Lines 1-20 of thirdparty.js (checking for getCredential import) ==="
sed -n '1,20p' src/api/routes/thirdparty.js

echo ""
echo "=== Lines 880-920 of thirdparty.js (getMercuryToken and mercuryFetch) ==="
sed -n '880,920p' src/api/routes/thirdparty.js

echo ""
echo "=== Lines 50-65 of thirdparty.js (checking argument count at line 55-59) ==="
sed -n '50,65p' src/api/routes/thirdparty.js

echo ""
echo "=== Lines 895-905 of thirdparty.js (Mercury token call with argument count) ==="
sed -n '895,905p' src/api/routes/thirdparty.js

Repository: chittyos/chittyconnect

Length of output: 3009


Remove redundant dynamic import of getCredential.

The module already imports getCredential at line 11. The dynamic import at line 897 is unnecessary and adds overhead.

Suggested fix
   // 4. Credential broker (1Password)
-  const { getCredential } = await import("../../lib/credential-helper.js");
   return getCredential(
     c.env,
     `integrations/mercury/${integrationSlug || "default"}`,
     "MERCURY_API_TOKEN",
     "Mercury",
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 4. Credential broker (1Password)
const { getCredential } = await import("../../lib/credential-helper.js");
return getCredential(
c.env,
`integrations/mercury/${integrationSlug || "default"}`,
"MERCURY_API_TOKEN",
"Mercury",
);
}
// 4. Credential broker (1Password)
return getCredential(
c.env,
`integrations/mercury/${integrationSlug || "default"}`,
"MERCURY_API_TOKEN",
"Mercury",
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/routes/thirdparty.js` around lines 896 - 904, The dynamic import of
getCredential inside the credential broker block is redundant because
getCredential is already statically imported; remove the await
import("../../lib/credential-helper.js") line and the const { getCredential } =
... declaration, and call the existing getCredential function directly in the
return statement (keep the same arguments: c.env,
`integrations/mercury/${integrationSlug || "default"}`, "MERCURY_API_TOKEN",
"Mercury") so there are no duplicate declarations or unnecessary runtime
imports.

Comment on lines +1008 to +1009
// Legacy aliases for ChittyFinance compatibility
// ChittyFinance calls /api/mercury/accounts → maps to /api/thirdparty/mercury/accounts
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incomplete or misleading comment.

This comment implies legacy aliases should be defined here, but they're implemented in router.js via the catch-all rewrite. Either remove this orphaned comment or add a reference to where the aliases are actually implemented.

📝 Proposed fix
-// Legacy aliases for ChittyFinance compatibility
-// ChittyFinance calls /api/mercury/accounts → maps to /api/thirdparty/mercury/accounts
+// Legacy aliases for ChittyFinance compatibility are handled in router.js
+// via catch-all rewrite: /api/mercury/* → /api/thirdparty/mercury/*
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Legacy aliases for ChittyFinance compatibility
// ChittyFinance calls /api/mercury/accountsmaps to /api/thirdparty/mercury/accounts
// Legacy aliases for ChittyFinance compatibility are handled in router.js
// via catch-all rewrite: /api/mercury/* → /api/thirdparty/mercury/*
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/routes/thirdparty.js` around lines 1008 - 1009, The comment block
saying "Legacy aliases for ChittyFinance" is orphaned and misleading; either
delete that comment from src/api/routes/thirdparty.js or replace it with a short
pointer stating that legacy aliasing is handled in router.js via the catch-all
rewrite (which maps /api/mercury/accounts → /api/thirdparty/mercury/accounts).
Update the comment to mention router.js and the catch-all rewrite (or remove it
entirely) so readers looking at thirdparty.js are not misled about where aliases
are implemented.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Mercury Banking API proxy endpoints to the existing third-party integration router, plus a legacy /api/mercury/* path intended for ChittyFinance compatibility.

Changes:

  • Add Mercury proxy endpoints under /api/thirdparty/mercury/* (accounts, account details, transactions, refresh/keepalive).
  • Add per-login API key resolution via slug/entity query/body with multiple fallback sources.
  • Add legacy rewrite route /api/mercury/* intended to forward into the third-party Mercury handlers.

Reviewed changes

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

File Description
src/api/routes/thirdparty.js Introduces Mercury API proxy helpers + routes and token resolution logic.
src/api/router.js Adds a legacy /api/mercury/* rewrite that dispatches to thirdpartyRoutes.

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

Comment on lines 870 to +873
export { thirdpartyRoutes };

// ── Mercury Banking API Proxy ────────────────────────────────────────
// Mercury API: https://api.mercury.com/api/v1
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

thirdpartyRoutes is already exported here, but the Mercury routes are defined after the export statement. While this still executes, it’s easy to miss and is inconsistent with the rest of the file structure; please move the export to the very end so all route registrations live above it.

Copilot uses AI. Check for mistakes.
Comment on lines +927 to +935
thirdpartyRoutes.get("/mercury/accounts", async (c) => {
try {
const slug = c.req.query("slug") || c.req.query("entity");
const token = await getMercuryToken(c, slug);
if (!token) {
return c.json({ error: "Mercury API token not configured for " + (slug || "default") }, 503);
}
const data = await mercuryFetch(token, "/accounts");
return c.json(data);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

The new Mercury routes/alias behavior isn’t covered by tests. This repo already has Vitest coverage for thirdparty routes; please add tests for token resolution and request forwarding for /mercury/accounts, /mercury/account/:id, /mercury/account/:id/transactions, and the legacy /api/mercury/* rewrite so regressions are caught before deploy.

Copilot generated this review using guidance from organization custom instructions.
Comment on lines +993 to +1004
try {
const body = await c.req.json().catch(() => ({}));
const slug = body.slug || c.req.query("slug") || c.req.query("entity");
const token = await getMercuryToken(c, slug);
if (!token) {
return c.json({ error: "Mercury API token not configured for " + (slug || "default") }, 503);
}
// Keepalive: just list accounts — any successful API call resets the 30-day timer
const data = await mercuryFetch(token, "/accounts");
return c.json({ ok: true, slug, accounts: data.accounts?.length || 0, timestamp: new Date().toISOString() });
} catch (error) {
return c.json({ error: error.message, slug: c.req.query("slug") }, 500);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

In the /mercury/refresh error response, slug is taken only from c.req.query("slug"), but in the success path slug may come from the JSON body. This can produce misleading error output when callers send {"slug": ...} in the body; consider defining slug outside the try/catch (or re-parsing in catch) so the error response reports the same resolved slug.

Suggested change
try {
const body = await c.req.json().catch(() => ({}));
const slug = body.slug || c.req.query("slug") || c.req.query("entity");
const token = await getMercuryToken(c, slug);
if (!token) {
return c.json({ error: "Mercury API token not configured for " + (slug || "default") }, 503);
}
// Keepalive: just list accounts — any successful API call resets the 30-day timer
const data = await mercuryFetch(token, "/accounts");
return c.json({ ok: true, slug, accounts: data.accounts?.length || 0, timestamp: new Date().toISOString() });
} catch (error) {
return c.json({ error: error.message, slug: c.req.query("slug") }, 500);
let slug;
try {
const body = await c.req.json().catch(() => ({}));
slug = body.slug || c.req.query("slug") || c.req.query("entity");
const token = await getMercuryToken(c, slug);
if (!token) {
return c.json(
{ error: "Mercury API token not configured for " + (slug || "default") },
503,
);
}
// Keepalive: just list accounts — any successful API call resets the 30-day timer
const data = await mercuryFetch(token, "/accounts");
return c.json({
ok: true,
slug,
accounts: data.accounts?.length || 0,
timestamp: new Date().toISOString(),
});
} catch (error) {
return c.json({ error: error.message, slug }, 500);

Copilot uses AI. Check for mistakes.
Comment on lines +158 to +161
// Rewrites to /api/thirdparty/mercury/* so the thirdparty handler picks them up.
api.all("/api/mercury/*", async (c) => {
const url = new URL(c.req.url);
url.pathname = url.pathname.replace("/api/mercury", "/api/thirdparty/mercury");
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

The rewrite builds a request whose pathname starts with /api/thirdparty/… and then calls thirdpartyRoutes.fetch(...) directly. Because thirdpartyRoutes is mounted under /api/thirdparty on the parent app, calling the sub-app’s fetch expects unprefixed paths (e.g. /mercury/accounts), so this rewrite will not match the Mercury handlers. Rewrite /api/mercury/*/mercury/* (or dispatch via the parent api.fetch instead) to ensure the routes are actually reachable.

Suggested change
// Rewrites to /api/thirdparty/mercury/* so the thirdparty handler picks them up.
api.all("/api/mercury/*", async (c) => {
const url = new URL(c.req.url);
url.pathname = url.pathname.replace("/api/mercury", "/api/thirdparty/mercury");
// Rewrites to /mercury/* so the thirdparty sub-app (mounted at /api/thirdparty) can match its internal routes.
api.all("/api/mercury/*", async (c) => {
const url = new URL(c.req.url);
url.pathname = url.pathname.replace("/api/mercury", "/mercury");

Copilot uses AI. Check for mistakes.
Comment on lines +889 to +903
// 2. Env secret by slug pattern: MERCURY_API_KEY_{SLUG}
const envKey = `MERCURY_API_KEY_${(integrationSlug || "default").toUpperCase()}`;
if (c.env[envKey]) return c.env[envKey];

// 3. Generic fallback
if (c.env.MERCURY_API_TOKEN) return c.env.MERCURY_API_TOKEN;

// 4. Credential broker (1Password)
const { getCredential } = await import("../../lib/credential-helper.js");
return getCredential(
c.env,
`integrations/mercury/${integrationSlug || "default"}`,
"MERCURY_API_TOKEN",
"Mercury",
);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

integrationSlug comes from the request and is interpolated into the credential broker path (integrations/mercury/${...}). Because getCredential ultimately builds a URL path (e.g. /v1/credentials/${credentialPath}), a slug containing ../ or / can be normalized into a different vault path and potentially expose unrelated secrets. Please strictly validate/whitelist allowed slugs (e.g. [a-z0-9_-]+ only) and/or build the credential path using fixed segments with encodeURIComponent on the slug component so path traversal is impossible.

Copilot uses AI. Check for mistakes.
Comment on lines +896 to +903
// 4. Credential broker (1Password)
const { getCredential } = await import("../../lib/credential-helper.js");
return getCredential(
c.env,
`integrations/mercury/${integrationSlug || "default"}`,
"MERCURY_API_TOKEN",
"Mercury",
);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

This file already has a top-level getCredential import, but getMercuryToken does a dynamic import and shadows the name. That adds unnecessary per-request work and makes the dependency harder to track; please reuse the existing import instead of importing inside the handler.

Copilot uses AI. Check for mistakes.
Comment on lines +911 to +912
Authorization: `Bearer ${token}`,
...options.headers,
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

mercuryFetch spreads ...options.headers after setting Authorization, which means any caller passing options.headers.Authorization will override the Mercury token. To avoid accidental/unsafe overrides, merge caller headers first and then set Authorization (and other required headers) last.

Suggested change
Authorization: `Bearer ${token}`,
...options.headers,
...(options.headers || {}),
Authorization: `Bearer ${token}`,

Copilot uses AI. Check for mistakes.
Comment on lines +882 to +887
* Checks: request header > env secret > credential broker
*/
async function getMercuryToken(c, integrationSlug) {
// 1. Explicit header (for testing)
const headerToken = c.req.header("X-Mercury-Token");
if (headerToken) return headerToken;
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

Accepting a raw Mercury token from X-Mercury-Token allows any authenticated caller of this proxy to supply arbitrary Mercury credentials (bypassing your secret/broker resolution and audit expectations). If this is only intended for local testing, please gate it behind a non-production flag and/or restrict it to an admin-only auth context.

Suggested change
* Checks: request header > env secret > credential broker
*/
async function getMercuryToken(c, integrationSlug) {
// 1. Explicit header (for testing)
const headerToken = c.req.header("X-Mercury-Token");
if (headerToken) return headerToken;
* Checks: (optionally) request header > env secret > credential broker
*/
async function getMercuryToken(c, integrationSlug) {
// 1. Explicit header (for testing in non-production only, behind a flag)
const headerToken = c.req.header("X-Mercury-Token");
const nodeEnv = (c.env && c.env.NODE_ENV) ? c.env.NODE_ENV : "production";
const allowHeaderOverride =
nodeEnv !== "production" &&
c.env &&
c.env.MERCURY_ALLOW_HEADER_OVERRIDE === "true";
if (allowHeaderOverride && headerToken) {
return headerToken;
}

Copilot uses AI. Check for mistakes.
@chitcommit
Copy link
Copy Markdown
Contributor Author

Duplicate of #129 which was already merged.

@chitcommit chitcommit closed this Mar 28, 2026
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.

2 participants