Skip to content

feat: Gmail proxy routes for evidence backfill#159

Open
chitcommit wants to merge 3 commits intomainfrom
feat/gmail-proxy
Open

feat: Gmail proxy routes for evidence backfill#159
chitcommit wants to merge 3 commits intomainfrom
feat/gmail-proxy

Conversation

@chitcommit
Copy link
Copy Markdown
Contributor

@chitcommit chitcommit commented Apr 23, 2026

Summary

  • Adds 3 Gmail API proxy endpoints under /api/thirdparty/gmail/*
  • GET /gmail/messages — list with query/pagination
  • GET /gmail/message/:messageId — metadata
  • GET /gmail/message/:messageId/raw — raw RFC 2822 .eml bytes (decoded from base64url)
  • Uses same Google OAuth2 access token as Calendar proxy (auto-rotated by SecretRotationService)
  • Required by chittyevidence-db Gmail backfill endpoint (POST /admin/backfill/gmail)

Test plan

  • Verify Google OAuth app has gmail.readonly scope
  • curl /api/thirdparty/gmail/messages?maxResults=1 returns message list
  • curl /api/thirdparty/gmail/message/{id}/raw returns RFC 2822 content
  • End-to-end: POST evidence.chitty.cc/admin/backfill/gmail?limit=1&dry=true succeeds

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Integrated Gmail support: list messages with query/filtering, view message details, and download individual messages in standard (RFC‑822) format
    • Format options for message retrieval (e.g., metadata vs full/raw)
  • Bug Fixes / Reliability

    • Improved error responses and clearer handling when Gmail access is unavailable or upstream requests fail

Adds three Gmail API proxy endpoints under /api/thirdparty/gmail/*:
- GET /gmail/messages — list messages with query/pagination
- GET /gmail/message/:messageId — get message metadata
- GET /gmail/message/:messageId/raw — get raw RFC 2822 .eml bytes

Uses the same Google OAuth2 access token (auto-rotated by
SecretRotationService) as the existing Google Calendar proxy.
Required by chittyevidence-db's Gmail backfill endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 23, 2026 15:08
@cloudflare-workers-and-pages
Copy link
Copy Markdown
Contributor

cloudflare-workers-and-pages Bot commented Apr 23, 2026

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 2fdfebd Apr 24 2026, 02:34 PM

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds a Gmail proxy under thirdpartyRoutes that reuses the existing Google OAuth2 credential to forward requests to Gmail REST endpoints, exposing message listing, message retrieval (with format validation), and raw message download (base64url-decoded as RFC 822 binary). Missing token yields 503; unexpected errors return 500 with { error: message }.

Changes

Cohort / File(s) Summary
Gmail Proxy Endpoints
src/api/routes/thirdparty.js
New Gmail proxy layer: shared gmailFetch helper, three GET endpoints: message list (q, maxResults, pageToken), single-message retrieval (format validated), and raw message download (format=raw, base64url → binary attachment). Returns 503 if Google token absent; propagates upstream non-OK statuses; unexpected errors return { error: message } with 500.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Server as Node.js Server
    participant Creds as Google OAuth Credential Store
    participant Gmail as Gmail REST API

    Client->>Server: GET /api/thirdparty/gmail/...
    Server->>Creds: getCredential('GOOGLE_ACCESS_TOKEN')
    Creds-->>Server: token or null

    alt token available
        Server->>Gmail: HTTP request to gmail.googleapis.com/gmail/v1 (Authorization: Bearer token)
        Gmail-->>Server: 2xx JSON or non-OK response
        alt 2xx
            Server-->>Client: 200 JSON or binary (raw decoded) response
        else non-OK
            Server-->>Client: Forward upstream status with error body
        end
    else token missing
        Server-->>Client: 503 Service Unavailable
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I tunneled paths to Gmail's shore,

Tokens found, I fetch and store,
Messages list and raw bytes stream,
Decoded crumbs of email dream,
Hop, hop — the proxy hums galore.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers purpose and scope well, but is missing required checklist sections for Security & Access, Docs, and Validation per the template. Complete the Security & Access checklist items, confirm docs updates (or note N/A), and add CI validation status with checkbox confirmations.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding Gmail proxy routes for evidence backfill. It is concise, specific, and directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/gmail-proxy

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: 1

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

1060-1076: Validate/encode the format query parameter.

format is interpolated directly into the Gmail URL without validation or encodeURIComponent. Although Gmail will reject unknown values, arbitrary user input flows into the outbound URL's query string. Prefer constraining it to the Gmail-documented values (minimal | full | raw | metadata) and/or encoding it. Same applies to messageId, which is trusted here — Gmail IDs are hex-ish so in practice safe, but encodeURIComponent is cheap insurance.

🛠️ Proposed fix
     const { messageId } = c.req.param();
-    const { format = "metadata" } = c.req.query();
+    const { format = "metadata" } = c.req.query();
+    const allowedFormats = new Set(["minimal", "full", "raw", "metadata"]);
+    if (!allowedFormats.has(format)) {
+      return c.json({ error: "Invalid format" }, 400);
+    }
@@
-    const response = await fetch(
-      `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=${format}`,
+    const response = await fetch(
+      `https://gmail.googleapis.com/gmail/v1/users/me/messages/${encodeURIComponent(messageId)}?format=${format}`,
       { headers: { Authorization: `Bearer ${googleToken}` } },
     );
🤖 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 1060 - 1076, Validate and encode
the user-controlled values before building the Gmail URL: restrict the `format`
value returned from c.req.query() to the allowed set
["minimal","full","raw","metadata"] (e.g., check membership and default to
"metadata" or return 400 for invalid), and use encodeURIComponent for both
`messageId` (from c.req.param()) and the validated `format` when constructing
the fetch URL (the fetch call that currently uses
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=${format}`
should use the encoded/validated values).

1011-1142: Consider extracting a gmailFetch helper to mirror the Mercury pattern.

The three handlers repeat the same getCredential lookup, 503 branch, fetch with Authorization header, and non-OK error shaping. The Mercury section in this file already demonstrates a cleaner pattern (mercuryFetch + requireMercuryToken middleware + mercuryHandler wrapper). A small gmailFetch(token, path) helper and a requireGoogleToken middleware would remove ~30 lines of duplication and make adding future Gmail endpoints trivial. Non-blocking.

🤖 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 1011 - 1142, The three Gmail
handlers (routes defined with thirdpartyRoutes.get for "/gmail/messages",
"/gmail/message/:messageId", and "/gmail/message/:messageId/raw") duplicate
credential lookup (getCredential), 503 handling, fetch with Authorization
header, and non-OK error shaping; implement a small helper
gmailFetch(googleToken, path, options?) that performs the fetch to
https://gmail.googleapis.com/gmail/v1{path} with the Authorization header and
throws a unified error on non-ok, and add a requireGoogleToken middleware (or
helper requireGoogleToken(c) that wraps handlers) which calls
getCredential("integrations/google/access_token", "GOOGLE_ACCESS_TOKEN") and
returns a 503 JSON when missing; then refactor the three handlers to call
requireGoogleToken to obtain the token and use gmailFetch(token,
"/users/me/messages...") (or gmailFetch with the message path) to remove
duplication and keep response handling (including raw decoding) in the
route-specific logic.
🤖 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/routes/thirdparty.js`:
- Around line 1124-1131: Gmail raw payloads are base64url without padding, so
after replacing '-'/'_' with '+'/'/' (the variable base64) you must append '='
padding until base64.length % 4 === 0 before calling atob; update the block that
sets base64 and calls atob (variables data.raw, base64, binaryString) to add
padding (e.g., while (base64.length % 4) base64 += '=') so atob won't throw
InvalidCharacterError and the subsequent Uint8Array creation remains unchanged.

---

Nitpick comments:
In `@src/api/routes/thirdparty.js`:
- Around line 1060-1076: Validate and encode the user-controlled values before
building the Gmail URL: restrict the `format` value returned from c.req.query()
to the allowed set ["minimal","full","raw","metadata"] (e.g., check membership
and default to "metadata" or return 400 for invalid), and use encodeURIComponent
for both `messageId` (from c.req.param()) and the validated `format` when
constructing the fetch URL (the fetch call that currently uses
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=${format}`
should use the encoded/validated values).
- Around line 1011-1142: The three Gmail handlers (routes defined with
thirdpartyRoutes.get for "/gmail/messages", "/gmail/message/:messageId", and
"/gmail/message/:messageId/raw") duplicate credential lookup (getCredential),
503 handling, fetch with Authorization header, and non-OK error shaping;
implement a small helper gmailFetch(googleToken, path, options?) that performs
the fetch to https://gmail.googleapis.com/gmail/v1{path} with the Authorization
header and throws a unified error on non-ok, and add a requireGoogleToken
middleware (or helper requireGoogleToken(c) that wraps handlers) which calls
getCredential("integrations/google/access_token", "GOOGLE_ACCESS_TOKEN") and
returns a 503 JSON when missing; then refactor the three handlers to call
requireGoogleToken to obtain the token and use gmailFetch(token,
"/users/me/messages...") (or gmailFetch with the message path) to remove
duplication and keep response handling (including raw decoding) in the
route-specific logic.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6029a3ef-f693-499b-a6d8-e99fe201c354

📥 Commits

Reviewing files that changed from the base of the PR and between 1d3e8a9 and 2777847.

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

Comment thread src/api/routes/thirdparty.js Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2777847f50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1139 to +1140
} catch (error) {
return c.json({ error: error.message }, 500);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return upstream Gmail status codes instead of always 500

When Gmail returns non-2xx responses (for example 401 expired token, 404 missing message, or 429 quota), this handler throws and the catch block always responds with HTTP 500. That masks actionable client/upstream conditions as server faults and can break backfill retry/skip behavior because callers cannot distinguish permanent not-found from transient internal errors.

Useful? React with 👍 / 👎.

Comment thread src/api/routes/thirdparty.js Outdated
}

const response = await fetch(
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=${format}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Encode Gmail params before interpolating request URL

This URL is built by directly interpolating untrusted messageId and format, so reserved characters can alter the outgoing request semantics. For example, a path value containing encoded ?/& can truncate or rewrite the Gmail request, resulting in fetching the wrong resource and inconsistent errors. Build the URL with encodeURIComponent for path segments and URLSearchParams for query values.

Useful? React with 👍 / 👎.

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 Gmail API proxy endpoints to the existing /api/thirdparty Hono router to support evidence backfill flows that need message listing, metadata retrieval, and raw RFC 2822 bytes using the existing Google OAuth2 token mechanism.

Changes:

  • Add GET /api/thirdparty/gmail/messages for listing messages with query/pagination.
  • Add GET /api/thirdparty/gmail/message/:messageId for message metadata retrieval.
  • Add GET /api/thirdparty/gmail/message/:messageId/raw for returning decoded message/rfc822 bytes.

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

Comment thread src/api/routes/thirdparty.js Outdated
Comment on lines +1124 to +1128
// Gmail returns base64url-encoded RFC 2822 message
// Convert base64url → base64 → binary
const base64 = data.raw.replace(/-/g, "+").replace(/_/g, "/");
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
Comment on lines +1134 to +1137
headers: {
"Content-Type": "message/rfc822",
"Content-Disposition": `attachment; filename="${messageId}.eml"`,
},
Comment thread src/api/routes/thirdparty.js Outdated
Comment on lines +1043 to +1046
if (!response.ok) {
const body = await response.text();
throw new Error(`Gmail API error: ${response.status} — ${body}`);
}
Comment thread src/api/routes/thirdparty.js Outdated
Comment on lines +1061 to +1076
const { format = "metadata" } = c.req.query();

const googleToken = await getCredential(
c.env,
"integrations/google/access_token",
"GOOGLE_ACCESS_TOKEN",
);

if (!googleToken) {
return c.json({ error: "Google access token not configured" }, 503);
}

const response = await fetch(
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=${format}`,
{ headers: { Authorization: `Bearer ${googleToken}` } },
);
Comment thread src/api/routes/thirdparty.js Outdated
Comment on lines +1016 to +1023
/**
* GET /api/thirdparty/gmail/messages
* List Gmail messages (metadata only)
*/
thirdpartyRoutes.get("/gmail/messages", async (c) => {
try {
const { q, maxResults = "10", pageToken } = c.req.query();

Comment on lines +1017 to +1018
* GET /api/thirdparty/gmail/messages
* List Gmail messages (metadata only)
@chitcommit chitcommit enabled auto-merge (squash) April 24, 2026 10:32
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • src/api/routes/thirdparty.js

Commit: 65915f30536a88341d8b0c6a32753a0ec674d173

The changes have been pushed to the feat/gmail-proxy branch.

Time taken: 2m 19s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
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 Gmail proxy endpoints to the existing /api/thirdparty integration router so downstream services (notably evidence backfill) can list messages, fetch message metadata, and download raw RFC 2822 .eml content using the existing Google OAuth2 access token mechanism.

Changes:

  • Add GET /gmail/messages for listing messages with query/pagination support.
  • Add GET /gmail/message/:messageId for fetching message details (format-selectable).
  • Add GET /gmail/message/:messageId/raw to fetch and decode Gmail raw content to RFC 822 bytes.

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

Comment thread src/api/routes/thirdparty.js Outdated
Comment on lines +1043 to +1051
if (!response.ok) {
const body = await response.text();
throw new Error(`Gmail API error: ${response.status} — ${body}`);
}

return c.json(await response.json());
} catch (error) {
return c.json({ error: error.message }, 500);
}
Comment thread src/api/routes/thirdparty.js Outdated
Comment on lines +1124 to +1132
// Gmail returns base64url-encoded RFC 2822 message
// Convert base64url → base64 → binary
let base64 = data.raw.replace(/-/g, "+").replace(/_/g, "/");
while (base64.length % 4) base64 += '=';
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
Comment on lines +1136 to +1138
"Content-Type": "message/rfc822",
"Content-Disposition": `attachment; filename="${messageId}.eml"`,
},
Comment on lines +1016 to +1052
/**
* GET /api/thirdparty/gmail/messages
* List Gmail messages (metadata only)
*/
thirdpartyRoutes.get("/gmail/messages", async (c) => {
try {
const { q, maxResults = "10", pageToken } = c.req.query();

const googleToken = await getCredential(
c.env,
"integrations/google/access_token",
"GOOGLE_ACCESS_TOKEN",
);

if (!googleToken) {
return c.json({ error: "Google access token not configured" }, 503);
}

const params = new URLSearchParams({ maxResults });
if (q) params.append("q", q);
if (pageToken) params.append("pageToken", pageToken);

const response = await fetch(
`https://gmail.googleapis.com/gmail/v1/users/me/messages?${params.toString()}`,
{ headers: { Authorization: `Bearer ${googleToken}` } },
);

if (!response.ok) {
const body = await response.text();
throw new Error(`Gmail API error: ${response.status} — ${body}`);
}

return c.json(await response.json());
} catch (error) {
return c.json({ error: error.message }, 500);
}
});
Comment thread src/api/routes/thirdparty.js Outdated
Comment on lines +1060 to +1075
const { messageId } = c.req.param();
const { format = "metadata" } = c.req.query();

const googleToken = await getCredential(
c.env,
"integrations/google/access_token",
"GOOGLE_ACCESS_TOKEN",
);

if (!googleToken) {
return c.json({ error: "Google access token not configured" }, 503);
}

const response = await fetch(
`https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=${format}`,
{ headers: { Authorization: `Bearer ${googleToken}` } },
@chitcommit
Copy link
Copy Markdown
Contributor Author

@claude review and address comments to resolve conversations.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 24, 2026

Claude finished @chitcommit's task in 2m 15s —— View job


Gmail proxy review comments addressed

  • Read current file state — CodeRabbit's autofix (65915f3) had already applied base64 padding
  • Fix upstream status code propagation — Gmail 401/403/404/429 now returned to caller instead of always 500
  • Fix format parameter injection — validated against GMAIL_ALLOWED_FORMATS set before URL interpolation
  • Fix messageId URL encoding — encodeURIComponent applied in both /message/:messageId and /message/:messageId/raw
  • Fix Content-Disposition header injection — messageId sanitized to [a-zA-Z0-9_\-] before use in filename
  • Refactor: extract gmailFetch() helper — removes ~30 lines of duplication across the three handlers; each now does one credential check then delegates to the helper
  • Commit and push — 2fdfebd

Summary of changes (src/api/routes/thirdparty.js)

Issue Fix
Upstream errors always returned 500 gmailFetch() returns { ok, status, errorBody } — handlers now do return c.json({...}, result.status)
format param injected verbatim into URL Validated against new Set(["minimal","full","raw","metadata"]), defaults to "metadata" if unknown
messageId path param unencoded in URL encodeURIComponent(messageId) applied at URL-build time
messageId unencoded in Content-Disposition messageId.replace(/[^a-zA-Z0-9_\-]/g, "_") before interpolation
Duplicated credential lookup + 503 + fetch in all 3 handlers Extracted into gmailFetch(googleToken, path) helper
base64url padding (already fixed by CodeRabbit 65915f3) "=".repeat((4 - b64.length % 4) % 4) retained

Not addressed: unit test coverage and OpenAPI spec update for the new routes (flagged by Copilot) — these would be follow-on PRs as they're additive and out of scope for this fix pass.
| Branch: feat/gmail-proxy

- Extract gmailFetch() helper to remove credential-check/fetch/error
  duplication across all three handlers
- Propagate upstream Gmail API status codes (401/403/404/429) instead of
  always returning 500, so backfill callers can distinguish transient vs
  permanent failures
- Validate `format` query param against allowed set before URL interpolation
- Apply encodeURIComponent to messageId path segment in both message endpoints
- Sanitize messageId to filename-safe chars in Content-Disposition header to
  prevent header injection
- Keep base64url padding fix from prior CodeRabbit autofix (65915f3)

Co-authored-by: @chitcommit <chitcommit@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Unsigned Commits Detected

This PR contains unsigned commits. ChittyOS requires all commits to be cryptographically signed.

How to fix this:

Option 1: Sign with SSH key (Recommended)

# Configure Git to use SSH signing
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

# Re-sign your commits
git rebase --exec 'git commit --amend --no-edit -S' HEAD~N
git push --force-with-lease

Option 2: Sign with 1Password

  1. Enable "Sign Git commits" in 1Password settings
  2. Configure Git: git config --global gpg.program /path/to/op-ssh-sign

Option 3: Sign with GPG

git config --global commit.gpgsign true
git config --global user.signingkey YOUR_KEY_ID

📚 1Password SSH Signing Guide

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: 2

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

1127-1131: Prefer the idiomatic Uint8Array.from(atob(...), c => c.charCodeAt(0)) pattern used elsewhere in this codebase.

src/api/routes/files.js:186 already uses the single-expression form for identical base64→bytes conversion. Adopting it here keeps decode logic consistent and drops five lines.

♻️ Proposed refactor
-    const binaryString = atob(padded);
-    const bytes = new Uint8Array(binaryString.length);
-    for (let i = 0; i < binaryString.length; i++) {
-      bytes[i] = binaryString.charCodeAt(i);
-    }
+    const bytes = Uint8Array.from(atob(padded), (ch) => ch.charCodeAt(0));
🤖 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 1127 - 1131, Replace the manual
loop that converts base64-decoded string to bytes (the binaryString/bytes loop
that fills bytes[i] = binaryString.charCodeAt(i)) with the idiomatic
single-expression form used elsewhere: create the Uint8Array with
Uint8Array.from(atob(padded), c => c.charCodeAt(0)); update the variable
assignment that currently sets bytes to the loop result to this one-liner so the
decode logic uses the consistent pattern.
🤖 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/routes/thirdparty.js`:
- Line 1134: The regex in the messageId.replace call used to create safeFilename
escapes a trailing hyphen unnecessarily, triggering ESLint no-useless-escape;
update the call to messageId.replace(...) used to compute safeFilename so the
character class no longer escapes the hyphen (remove the backslash before -),
keeping the rest of the sanitization intact.
- Around line 1018-1027: The gmailFetch helper currently discards Gmail's error
payload (it reads response.text() into errorBody but callers only use
result.status), so modify gmailFetch to return the upstream error body to
callers: on non-OK responses attempt to parse await response.text() as JSON
(fall back to the raw text) and return { ok: false, status: response.status,
errorBodyRaw: text, errorBodyJson: parsedOrNull } (keep response out to avoid
streams). Then update the callers that inspect the result (the places reading
result.status and returning "Gmail API error: ${result.status}") to include the
parsed error details (or raw text) in their error messages/logs so the Gmail
{error:{code,message,errors}} payload is surfaced to logs and responses.

---

Nitpick comments:
In `@src/api/routes/thirdparty.js`:
- Around line 1127-1131: Replace the manual loop that converts base64-decoded
string to bytes (the binaryString/bytes loop that fills bytes[i] =
binaryString.charCodeAt(i)) with the idiomatic single-expression form used
elsewhere: create the Uint8Array with Uint8Array.from(atob(padded), c =>
c.charCodeAt(0)); update the variable assignment that currently sets bytes to
the loop result to this one-liner so the decode logic uses the consistent
pattern.
🪄 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: 744cc16c-f036-48d3-a339-6d69f55a8801

📥 Commits

Reviewing files that changed from the base of the PR and between 2777847 and 2fdfebd.

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

Comment on lines +1018 to +1027
async function gmailFetch(googleToken, path) {
const response = await fetch(
`https://gmail.googleapis.com/gmail/v1${path}`,
{ headers: { Authorization: `Bearer ${googleToken}` } },
);
if (!response.ok) {
return { ok: false, status: response.status, errorBody: await response.text() };
}
return { ok: true, response };
}
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

errorBody is read but never surfaced — upstream failure context is lost.

gmailFetch consumes response.text() into errorBody on the non-OK path, but every caller (lines 1051, 1083, 1115) only reads result.status and responds with "Gmail API error: ${result.status}". The body — which typically contains Gmail's structured { error: { code, message, errors: [...] } } — is discarded, making 400/401/403/429 responses hard to diagnose in production (and in the chittyevidence-db backfill path).

🛠️ Suggested fix — surface the upstream body (optionally parsed as JSON) to callers
 async function gmailFetch(googleToken, path) {
   const response = await fetch(
     `https://gmail.googleapis.com/gmail/v1${path}`,
     { headers: { Authorization: `Bearer ${googleToken}` } },
   );
   if (!response.ok) {
-    return { ok: false, status: response.status, errorBody: await response.text() };
+    const errorBody = await response.text().catch(() => "");
+    let errorDetails;
+    try { errorDetails = JSON.parse(errorBody)?.error; } catch { /* non-JSON body */ }
+    return { ok: false, status: response.status, errorBody, errorDetails };
   }
   return { ok: true, response };
 }

Then in each caller:

-    if (!result.ok) {
-      return c.json({ error: `Gmail API error: ${result.status}` }, result.status);
-    }
+    if (!result.ok) {
+      return c.json(
+        { error: `Gmail API error: ${result.status}`, details: result.errorDetails ?? result.errorBody?.slice(0, 500) },
+        result.status,
+      );
+    }
🤖 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 1018 - 1027, The gmailFetch helper
currently discards Gmail's error payload (it reads response.text() into
errorBody but callers only use result.status), so modify gmailFetch to return
the upstream error body to callers: on non-OK responses attempt to parse await
response.text() as JSON (fall back to the raw text) and return { ok: false,
status: response.status, errorBodyRaw: text, errorBodyJson: parsedOrNull } (keep
response out to avoid streams). Then update the callers that inspect the result
(the places reading result.status and returning "Gmail API error:
${result.status}") to include the parsed error details (or raw text) in their
error messages/logs so the Gmail {error:{code,message,errors}} payload is
surfaced to logs and responses.

}

// Strip non-filename-safe chars from messageId to prevent header injection
const safeFilename = messageId.replace(/[^a-zA-Z0-9_\-]/g, "_");
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

CI blocker: ESLint no-useless-escape on \- inside character class.

Pipeline fails here. Inside a character class, - at the trailing position doesn't need escaping — drop the backslash.

🛠️ Proposed fix
-    const safeFilename = messageId.replace(/[^a-zA-Z0-9_\-]/g, "_");
+    const safeFilename = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");

As per coding guidelines: "Use ESLint for linting and Prettier for code formatting, enforced via npm run lint and npm run format".

📝 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
const safeFilename = messageId.replace(/[^a-zA-Z0-9_\-]/g, "_");
const safeFilename = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
🧰 Tools
🪛 ESLint

[error] 1134-1134: Unnecessary escape character: -.

(no-useless-escape)

🪛 GitHub Actions: ChittyConnect CI

[error] 1134-1134: ESLint (no-useless-escape): Unnecessary escape character: -

🪛 GitHub Check: Lint (20)

[failure] 1134-1134:
Unnecessary escape character: -

🪛 GitHub Check: Lint (22)

[failure] 1134-1134:
Unnecessary escape character: -

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

In `@src/api/routes/thirdparty.js` at line 1134, The regex in the
messageId.replace call used to create safeFilename escapes a trailing hyphen
unnecessarily, triggering ESLint no-useless-escape; update the call to
messageId.replace(...) used to compute safeFilename so the character class no
longer escapes the hyphen (remove the backslash before -), keeping the rest of
the sanitization intact.

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