# 14-Jul-2026 — replace SheetJS xlsx with exceljs [ref #315](https://github.com/nbarrett/ngx-ramblers/issues/315)

## [build 776](https://github.com/nbarrett/ngx-ramblers/actions/runs/29354643657) — [commit 3dda3b9](https://github.com/nbarrett/ngx-ramblers/commit/3dda3b9f4c5171722c035354e80aedf1dc89f505)

_____

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:

- prototype pollution  GHSA-4r6h-8v6p-xvw6  (< 0.19.3)
- ReDoS                GHSA-5pgg-2g8v-p4x9  (< 0.20.2)

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]:

- auto-hyperlinked email  ->  {text, hyperlink}
- formula                 ->  {formula, result}
- rich text               ->  {richText: [...]}
- date                    ->  Date, where xlsx gave an Excel serial

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.