feat(watcher): gate receipt watch with WATCHER_MAILBOXES

Limit Pub/Sub INBOX watch and receipt processing to an allowlisted mailbox set; leave the agent CLI usable for all connected accounts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 10:09:25 +07:00
co-authored by Cursor
parent bac8edfb6c
commit 770ed8121a
5 changed files with 115 additions and 3 deletions
+30
View File
@@ -20,6 +20,36 @@ export const GOOGLE_CLOUD_PROJECT =
export const PUBSUB_VERIFICATION_TOKEN =
process.env.PUBSUB_VERIFICATION_TOKEN || "";
export const SQLITE_PATH = process.env.SQLITE_PATH || defaultSqlitePath;
/** Optional default mailbox email when scripts omit --user */
export const GMAIL_DEFAULT_USER = (process.env.GMAIL_DEFAULT_USER || "").trim();
/**
* Parse comma-separated mailbox allowlist for the receipt watcher.
* Empty / unset → empty Set (caller treats as "allow all").
*/
export function parseWatcherMailboxes(raw) {
const set = new Set();
for (const part of String(raw || "").split(",")) {
const email = part.trim().toLowerCase();
if (email) set.add(email);
}
return set;
}
/** Lowercased emails allowed for Pub/Sub receipt watch. Empty = all connected accounts. */
export const WATCHER_MAILBOXES = parseWatcherMailboxes(
process.env.WATCHER_MAILBOXES,
);
/**
* @param {string|null|undefined} email
* @param {Set<string>} [allowlist=WATCHER_MAILBOXES]
*/
export function isWatcherMailboxAllowed(email, allowlist = WATCHER_MAILBOXES) {
if (!allowlist || allowlist.size === 0) return true;
if (!email) return false;
return allowlist.has(String(email).trim().toLowerCase());
}
export const POCKETBASE_URL = (process.env.POCKETBASE_URL || "").replace(
/\/$/,
+23 -1
View File
@@ -1,5 +1,12 @@
import Fastify from "fastify";
import { GOOGLE_PUBSUB_TOPIC, PORT, WATCH_RENEW_MS, assertConfig } from "./config.js";
import {
GOOGLE_PUBSUB_TOPIC,
PORT,
WATCH_RENEW_MS,
WATCHER_MAILBOXES,
assertConfig,
isWatcherMailboxAllowed,
} from "./config.js";
import { listUsersWithTokens } from "./db.js";
import { startWatch } from "./google.js";
import { fetchAndLogMessage } from "./handlers/fetch-and-log-message.js";
@@ -44,7 +51,22 @@ async function renewAllWatches() {
app.log.warn("gmail-watch: no signed-in users with tokens");
return;
}
if (WATCHER_MAILBOXES.size > 0) {
app.log.info(
{ allowlist: [...WATCHER_MAILBOXES] },
"gmail-watch: WATCHER_MAILBOXES restrict receipt watches",
);
}
for (const user of users) {
if (!isWatcherMailboxAllowed(user.email)) {
app.log.info(
{ userId: user.id, email: user.email },
"gmail-watch: skip watch (not in WATCHER_MAILBOXES)",
);
continue;
}
try {
await startWatch(user.id);
app.log.info(
+9 -1
View File
@@ -1,5 +1,5 @@
import { timingSafeEqual } from "node:crypto";
import { PUBSUB_VERIFICATION_TOKEN } from "../config.js";
import { PUBSUB_VERIFICATION_TOKEN, isWatcherMailboxAllowed } from "../config.js";
import { getUserByEmail, getWatchState, saveWatchState } from "../db.js";
import {
gmailClientForUser,
@@ -71,6 +71,14 @@ function decodeNotification(body) {
}
async function processNotification(log, { fetchHandlers, processHandlers }, notification) {
if (!isWatcherMailboxAllowed(notification.emailAddress)) {
log.warn(
{ email: notification.emailAddress },
"gmail-watch: mailbox not in WATCHER_MAILBOXES, acking",
);
return;
}
const user = getUserByEmail(notification.emailAddress);
if (!user) {
log.warn(
@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
isWatcherMailboxAllowed,
parseWatcherMailboxes,
} from "../src/config.js";
describe("parseWatcherMailboxes", () => {
it("returns empty set for blank input", () => {
assert.equal(parseWatcherMailboxes("").size, 0);
assert.equal(parseWatcherMailboxes(null).size, 0);
assert.equal(parseWatcherMailboxes(" , ").size, 0);
});
it("normalizes and splits emails", () => {
const set = parseWatcherMailboxes(
" Eleven16th@gmail.com , other@Example.COM ",
);
assert.deepEqual([...set].sort(), [
"eleven16th@gmail.com",
"other@example.com",
]);
});
});
describe("isWatcherMailboxAllowed", () => {
it("allows all when allowlist is empty", () => {
const empty = new Set();
assert.equal(isWatcherMailboxAllowed("anyone@x.com", empty), true);
assert.equal(isWatcherMailboxAllowed(null, empty), true);
});
it("requires membership when allowlist is set", () => {
const allow = parseWatcherMailboxes("eleven16th@gmail.com");
assert.equal(
isWatcherMailboxAllowed("eleven16th@gmail.com", allow),
true,
);
assert.equal(
isWatcherMailboxAllowed("Eleven16th@Gmail.com", allow),
true,
);
assert.equal(isWatcherMailboxAllowed("other@gmail.com", allow), false);
assert.equal(isWatcherMailboxAllowed(null, allow), false);
});
});