The Docker build for the worker image failed at npm ci --ignore-scripts with a
403 from cdn.sheetjs.com. SheetJS distributes off the npm registry via their own
CDN, and that CDN blocks datacentre IP ranges, so the tarball URL dependency can
be fetched from a developer machine but not from a GitHub Actions runner.
Re-pinning to the registry was not a way out. xlsx@0.18.5 is the last version SheetJS published to npm and carries two unpatched high-severity advisories that npm audit reports as fixAvailable: false, because there is nothing newer on the registry to upgrade to:
The prototype pollution advisory is reachable: member-bulk-load parses a workbook the user has just uploaded. So the library is replaced rather than re-pinned.
exceljs is npm-hosted, maintained and free of known advisories. It was the single consumer in the codebase, at one call site.
To show the swap is behaviour-neutral, regression tests were written against the existing xlsx reader and confirmed green first, then re-run unchanged against exceljs. That caught four cell types which sheet_to_json silently flattens but exceljs returns as objects, and which a direct swap would have written into member records as [object Object]:
Excel auto-hyperlinks email addresses, so the first of those was a live risk on a real Ramblers export. The new reader normalises all four, and a test asserts no raw cell object can reach a row value.
One divergence cannot be removed: exceljs collapses an empty-string cell to null, so the empty-string versus absent-key distinction is lost. Rather than assume that was harmless, the row-to-member mapping was extracted and tested directly: every field passes through trim(), and trim(undefined) === trim("") === "", so the member records are identical either way.
Excel date cells are still read as serial numbers, as they were before. Real Ramblers exports store dates as strings, so this has never been hit. Preserved bug-for-bug so a security fix does not also change data handling.