feat: Gmail proxy routes for evidence backfill#159
Conversation
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>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
chittyconnect | 2fdfebd | Apr 24 2026, 02:34 PM |
📝 WalkthroughWalkthroughAdds a Gmail proxy under Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/api/routes/thirdparty.js (2)
1060-1076: Validate/encode theformatquery parameter.
formatis interpolated directly into the Gmail URL without validation orencodeURIComponent. 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 tomessageId, which is trusted here — Gmail IDs are hex-ish so in practice safe, butencodeURIComponentis 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 agmailFetchhelper to mirror the Mercury pattern.The three handlers repeat the same
getCredentiallookup, 503 branch, fetch withAuthorizationheader, and non-OK error shaping. The Mercury section in this file already demonstrates a cleaner pattern (mercuryFetch+requireMercuryTokenmiddleware +mercuryHandlerwrapper). A smallgmailFetch(token, path)helper and arequireGoogleTokenmiddleware 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
📒 Files selected for processing (1)
src/api/routes/thirdparty.js
There was a problem hiding this comment.
💡 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".
| } catch (error) { | ||
| return c.json({ error: error.message }, 500); |
There was a problem hiding this comment.
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 👍 / 👎.
| } | ||
|
|
||
| const response = await fetch( | ||
| `https://gmail.googleapis.com/gmail/v1/users/me/messages/${messageId}?format=${format}`, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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/messagesfor listing messages with query/pagination. - Add
GET /api/thirdparty/gmail/message/:messageIdfor message metadata retrieval. - Add
GET /api/thirdparty/gmail/message/:messageId/rawfor returning decodedmessage/rfc822bytes.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 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); |
| headers: { | ||
| "Content-Type": "message/rfc822", | ||
| "Content-Disposition": `attachment; filename="${messageId}.eml"`, | ||
| }, |
| if (!response.ok) { | ||
| const body = await response.text(); | ||
| throw new Error(`Gmail API error: ${response.status} — ${body}`); | ||
| } |
| 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}` } }, | ||
| ); |
| /** | ||
| * 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(); | ||
|
|
| * GET /api/thirdparty/gmail/messages | ||
| * List Gmail messages (metadata only) |
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
There was a problem hiding this comment.
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/messagesfor listing messages with query/pagination support. - Add
GET /gmail/message/:messageIdfor fetching message details (format-selectable). - Add
GET /gmail/message/:messageId/rawto fetch and decode Gmailrawcontent to RFC 822 bytes.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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); | ||
| } |
| // 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); | ||
| } |
| "Content-Type": "message/rfc822", | ||
| "Content-Disposition": `attachment; filename="${messageId}.eml"`, | ||
| }, |
| /** | ||
| * 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); | ||
| } | ||
| }); |
| 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}` } }, |
|
@claude review and address comments to resolve conversations. |
|
Claude finished @chitcommit's task in 2m 15s —— View job Gmail proxy review comments addressed
Summary of changes (
|
| 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>
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/api/routes/thirdparty.js (1)
1127-1131: Prefer the idiomaticUint8Array.from(atob(...), c => c.charCodeAt(0))pattern used elsewhere in this codebase.
src/api/routes/files.js:186already 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
📒 Files selected for processing (1)
src/api/routes/thirdparty.js
| 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 }; | ||
| } |
There was a problem hiding this comment.
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, "_"); |
There was a problem hiding this comment.
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.
| 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.
Summary
/api/thirdparty/gmail/*GET /gmail/messages— list with query/paginationGET /gmail/message/:messageId— metadataGET /gmail/message/:messageId/raw— raw RFC 2822 .eml bytes (decoded from base64url)chittyevidence-dbGmail backfill endpoint (POST /admin/backfill/gmail)Test plan
gmail.readonlyscopecurl /api/thirdparty/gmail/messages?maxResults=1returns message listcurl /api/thirdparty/gmail/message/{id}/rawreturns RFC 2822 contentPOST evidence.chitty.cc/admin/backfill/gmail?limit=1&dry=truesucceeds🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes / Reliability