diff --git a/src/lib/services/box/box-inbound-service.ts b/src/lib/services/box/box-inbound-service.ts index 37caff7..78a77e5 100644 --- a/src/lib/services/box/box-inbound-service.ts +++ b/src/lib/services/box/box-inbound-service.ts @@ -6,10 +6,42 @@ import { ingestInboundFile, type IngestResult } from "@/lib/services/inbound-ing * the file's name from Box (so the matcher has something to parse) and * delegates everything else — regex resolution, project/deliverable * matching, holding-pen vs active branching, notifications. + * + * Loop guard: only accepts events whose file's parent folder matches + * BOX_WATCH_FOLDER_ID. Box webhooks are already folder-scoped on + * subscription, but a misconfigured subscription on the outbound folder + * would otherwise ingest our own uploads. Defence in depth. */ export async function ingestBoxUpload(fileId: string): Promise { try { const meta = await getBoxFileMetadata(fileId); + + const watchFolderId = process.env.BOX_WATCH_FOLDER_ID; + const outFolderId = process.env.BOX_OUT_FOLDER_ID; + const parentId = meta.parent?.id; + + // Reject anything from the outbound folder — that's our own writes. + if (outFolderId && parentId === outFolderId) { + return { + ok: true, + status: "UNMATCHED", + error: `Ignoring event from outbound folder ${outFolderId} (loop guard).`, + }; + } + + // When the watch folder is configured, require the file to be a direct + // child of it. (We don't walk up the parent chain — files dropped + // into nested sub-folders won't fire matching webhooks anyway because + // Box only fires on the subscribed folder's direct contents unless + // the subscription includes descendants.) + if (watchFolderId && parentId !== watchFolderId) { + return { + ok: true, + status: "UNMATCHED", + error: `File parent ${parentId} is not the watch folder ${watchFolderId}.`, + }; + } + return ingestInboundFile({ source: "BOX", fileName: meta.name,