file-ingest-contracts
Installation
SKILL.md
File ingest contracts
Getting a file into the page is a chain of browser contracts: cancel the right drag events, read from the right DataTransfer surface, distrust the file's self-reported type, and revoke every preview URL you mint. Each link passes a happy-path demo and fails only with nested drop zones, dropped folders, spoofed types, clipboard pastes, or long sessions.
Checklist (lead with the trap; test shapes and evidence framing in references/)
- Cancel dragover or the drop never fires — and guard the window. Cancel
dragoverto make a custom element a drop target, then canceldropto suppress the browser's default handling. Browsers generally do not require cancelingdragenter, even though the drag-and-drop specification describes that step; do not use it as the rationale for whetherdropcan fire. Miss the required cancellation and a file dropped on (or near) the zone can make the browser navigate to / open the file and throw away the page. Add a window-leveldragover+droppreventDefault(or scope-check the target) so a near-miss cannot blow away unsaved state. - Toggle the highlight on a dragenter/dragleave counter, not raw events.
dragenterfires on every child element anddragleavefires when the pointer crosses from the parent into a child, so a naive add/remove-class flickers over nested content. Keep an integer:++ondragenter,--ondragleave, add the highlight when it reaches 1, clear when it returns to 0. Toggle ondragenter, notdragover(dragoverrefires every few hundred ms and forces needless repaints). - Re-set dropEffect on every dragover.
dropEffectdescribes the desired effect for that one dispatch and reverts next tick; setdataTransfer.dropEffect = 'copy'inside thedragoverhandler each time or the cursor won't reflect the operation. UsedropEffect = 'none'to visibly reject a spot. - Read folders through items + webkitGetAsEntry, not files.
DataTransfer.filesis a flatFileListwith no directory support; onlyDataTransfer.items→DataTransferItem.webkitGetAsEntry()(orgetAsFileSystemHandle()) can recurse into a dropped directory, and both are non-standard — feature-detect ('getAsFileSystemHandle' in DataTransferItem.prototype) and fall back.itemsis only live inside thedrop/dragstarthandler, so capture entries synchronously before anyawait. For click-to-select folders use<input webkitdirectory multiple>; it can't also pick loose files and shows a browser trust prompt. - Treat accept and file.type as UX hints, never a type gate.
acceptonly filters the OS picker (MDN is explicit: it does not validate) and drag-drop/paste bypass it entirely.file.typeis guessed from the extension, is spoofable, and is often an empty string. For a real check, sniff magic bytes client-side (Blob.slice(0, N)→arrayBuffer/FileReader→ compare the signature) as a fast pre-filter, and re-validate on the server — that is the trust boundary. Header-only sniffing is still defeated by polyglots. Route active SVG/HTML rendering risk tofrontend-security-baseline; route frontend-owned server upload limits, multipart reconstruction, and relay policy tobff-proxy-security-contracts. - Extract pasted images from clipboardData.items and bound count/size. On the
pasteevent, iterateevent.clipboardData.items, take entries wherekind === 'file'(ortypestarts withimage/), and callgetAsFile(). A single paste can carry both text and an image, so decide precedence rather than assuming one. Enforcefiles.lengthandfile.sizelimits before you read —accept/multipledo not bound how many or how large. - One object URL per preview, and revoke it.
URL.createObjectURLmints a new blob URL every call and pins the blob in memory untilURL.revokeObjectURL; re-creating one per render leaks. Revoke when the preview is replaced and on unmount (React:useEffectcleanup) — but not inside theimgonloadhandler, which breaks right-click / open-in-new-tab. The browser only frees these on document unload, so long single-page sessions accumulate them.
Quick probes
Use as leads; confirm the source-to-sink path before filing.