# Technical documentation — nodehtmltopdf

Evidence date: 2026-10-08. Revision: `master` `dc92bed` (2022-02-09).

Credential values from `cred.json` are omitted. Bucket names, the region, the secret id, and the response URL host are taken from source.

## What this repository is

A Bitbucket Git repository of a small Node.js Express service. `POST /htmltopdf` turns an HTML string into a PDF with `html-pdf` (PhantomJS) and uploads that file to S3.

There is one branch, `master`. Eighteen commits, from 2021-10-04 through 2022-02-09. Nine tracked files. `node_modules/` is gitignored and is not in the tree.

Authors: Jaco Brits (`jaco@overdrive.co.za` on four commits, `jaco@netstart.co.za` on the rest).

`sonar-project.properties` and `sonarqube_scan.bat` were listed by the onboarding scan and are not in git history.

## Repository layout

```text
nodehtmltopdf/
  index.js             Express entry. npm start. Loads cred.json
  index_wit SM.js      Same routes, Secrets Manager credentials. Not npm start
  SecretsManager.js    getSecret(secretName, region)
  config.js            bucket string and hewer logger. Required, not used
  cred.json            accessKeyId, secretAccessKey, region. Values omitted
  package.json         name htmlpdfapi, version 1.0.0
  package-lock.json    lockfileVersion 1
  README.md            Product note and Amazon Linux install
  .gitignore           node_modules, dist, plus a stock template list
```

The filename `index_wit SM.js` contains a space. It was added in `dc92bed`.

No Dockerfile, `bitbucket-pipelines.yml`, PM2 ecosystem file, test directory, or infrastructure template.

## Runtime

`package.json`:

| Script | Command | Result |
|---|---|---|
| `start` | `node index.js` | Runs the Express process |
| `test` | `echo "Error: no test specified" && exit 1` | Always fails |
| `deploy` | `npm run build && npm run start` | Fails. There is no `build` script |

No `engines` field. The README install pipes NodeSource `setup_16.x` and installs `nodejs` with `yum`. **Documented** Node 16. The version on a live host is **Unknown / Requires Confirmation.**

Listen port is the literal `8080` in both entry files. At `9025de3` the port was `3000`.

Module scope in `index.js` holds the Express app, the S3 client, and the loaded AWS config. The process is a long-running server, not a Lambda handler. Nothing in this repo exports `handler`.

## HTTP contract (`index.js`)

| Method and path | Behaviour |
|---|---|
| `POST /htmltopdf` | If `req.body.html` is missing, log and return without `res.send`. Otherwise start `html-pdf`, then `res.send({ filename, path })` immediately |
| `POST /pdf` | `res.send(req.body)`. No PDF |
| `GET /` | Log `PMF PDF Generator`, then `res.send(req.body)` |

Body parser: `bodyParser.urlencoded({ extended: false })` and `bodyParser.json()`. No custom limit.

CORS: `app.use(cors())` with no arguments.

`POST /htmltopdf` fields:

| Field | Read | Effect at HEAD |
|---|---|---|
| `html` | Required (truthy) | Passed to `pdf.create` |
| `pagesize` | `req.body.pagesize \|\| 'a4'` | None. Options `format` is `'Letter'` |
| `data` | Assigned to a local | None |

Response body on the success path:

```json
{
  "filename": "<base36>.pdf",
  "path": "https://tmt-pmf-prod-invoices.s3.eu-west-1.amazonaws.com/<base36>.pdf"
}
```

`filename` is `` `${Math.random().toString(36).slice(2)}.pdf` ``.

The `toFile` callback's parameter is also named `res`, which shadows the Express response inside that callback. The HTTP `res.send` has already run outside the callback, so the shadow does not block the response. The html-pdf result object is unused.

There is no `express` error middleware. `throw err` in `fs.readFile` and in `S3.upload` leaves the Express response path.

## PDF rendering

Library: `html-pdf` 3.0.1 (`package-lock.json`). Call:

```js
pdf.create(html, { format: 'Letter' }).toFile(filename, callback)
```

`phantomPath` is commented:

```js
//"phantomPath": "./node_modules/phantomjs/bin/phantomjs"
```

So the code does not force a PhantomJS binary. `html-pdf` 3 depends on PhantomJS. This repo also depends on `phantomjs` 2.1.7 and `phantomjs-prebuilt` 2.1.4. The README installs a third binary, PhantomJS 2.1.1 for Linux x86_64, and symlinks it to `/usr/bin/phantomjs`. Which binary a host actually executes is **Unknown / Requires Confirmation.**

`wkhtmltopdf` 0.4.0 is a dependency and is not required by application code.

The file is written to the process current directory, then read back. It is not removed. A commented `/tmp/` path existed in `9025de3` and is gone at HEAD.

## AWS

### Running entry

`AWS.config.loadFromPath('./cred.json')` at load time, then `new AWS.S3()`.

`cred.json` keys: `accessKeyId`, `secretAccessKey`, `region`. Region value: `eu-west-1`. The two key strings are omitted. The file is 127 bytes and is tracked. History rewrites it in `16d9273` and `b781463`. `24d9561` deletes it. `dc92bed` adds it back.

Upload parameters in `index.js`:

| Parameter | Value |
|---|---|
| `Bucket` | `tmt-pmf-prod-invoices` |
| `Key` | the generated filename |
| `Body` | buffer from `fs.readFile` |
| `contentType` | `application/pdf` |
| `ACL` | not set. The line `//ACL: 'public-read'` remains |

`config.js` exports `bucket: 'tmt-pmf-prod-invoices'` and is not consulted.

Bucket history on the upload call:

| Revision | Bucket |
|---|---|
| `9025de3` | `pmf-invoices` |
| `eacf1df` | `pmf-development-invoices` |
| `4ae97fd` through HEAD | `tmt-pmf-prod-invoices` |

From `c575fc2` until `4ae97fd`, the JSON `path` host was already `tmt-pmf-prod-invoices` while the upload bucket was still `pmf-development-invoices`.

SDK package: `aws-sdk` 2.1072.0. This is the v2 monolithic SDK, not `@aws-sdk/client-s3`.

### Secrets Manager variant

`SecretsManager.js` builds `new AWS.SecretsManager({ region })` and calls `getSecretValue({ SecretId: secretName }).promise()`.

`index_wit SM.js` calls `getSecret('pmf-prod-s3-user', 'eu-west-1')` inside `app.listen`, then on each `POST /htmltopdf` parses that string and reads `Access_key_ID`, `Secret_access_key`, and `Invoice_Bucket`. It passes a plain object to `AWS.config.loadFromPath`. That method reads a path from disk. **Inferred** that this call does not apply the object. The S3 client is constructed after that call, inside the request.

`index.js` requires `SecretsManager.js` and never calls it. `npm start` does not run `index_wit SM.js`.

Default AWS credential chain for a process started as `index_wit SM.js` would be whatever the host provides, because that file does not load `cred.json`. **Unknown / Requires Confirmation** if that file was ever the production entry.

## Process supervision and logs

README Testing: "Everything is logged in CloudWatch, and on instance there is a cli command : pm2 log".

README Install ends with `pm2 start index.html` and `pm2 log`. `package.json` depends on `pm2` 5.1.2 and does not invoke it. There is no ecosystem file. Application logs are `console.log`.

No CloudWatch agent, log group, or `aws-sdk` CloudWatch client appears in the source.

## Host install, as written

README Requirements: "AWS AEC2 , S3 permissions , Lamda Access via same Security Groups if seperate".

README Install commands, in order, on `yum` (Amazon Linux):

1. `sudo yum install -y gcc-c++ make`
2. NodeSource `setup_16.x`, then `sudo yum install -y nodejs`
3. `sudo yum install git`
4. `sudo npm install pm2@latest -g`
5. `wget` of `phantomjs-2.1.1-linux-x86_64.tar.bz2` from Bitbucket downloads
6. `ln -s` from `/usr/local/share/phantomjs-2.1.1-linux-x86_64/bin/phantomjs` to `/usr/bin/phantomjs`
7. `sudo yum install glibc fontconfig freetype freetype-devel fontconfig-devel wget bzip2`
8. `git clone https://github.com/TMTServices/pmf-pdfgeneration.git`
9. `cd pmf-pdfgeneration/ && npm install && pm2 start index.html && pm2 log`

The clone URL is not this Bitbucket remote. The PM2 script name is not `index.js`. The symlink runs before the `yum` line that installs `bzip2`, and the README never shows `tar`. **Documented** procedure. Whether it was followed is **Unknown / Requires Confirmation.**

## Dependencies

Direct application requires and lockfile versions:

| Package | Lockfile | Required by |
|---|---|---|
| `express` | 4.17.1 | `index.js`, `index_wit SM.js` |
| `body-parser` | 1.19.0 | both entry files |
| `cors` | 2.8.5 | both entry files |
| `html-pdf` | 3.0.1 | both entry files |
| `aws-sdk` | 2.1072.0 | both entry files, `SecretsManager.js` |
| `hewer` | 0.4.0 | `config.js` only |
| `phantomjs-prebuilt` | 2.1.4 | not required() in app code |
| `phantomjs` | 2.1.7 | not required() in app code |
| `wkhtmltopdf` | 0.4.0 | not required() in app code |
| `pm2` | 5.1.2 | not required() in app code |
| `memorystream` | 0.3.1 | not required() in app code |

Node core modules used: `fs`. `SecretsManager.js` sets `'use strict'`.

`package.json` `license` is `ISC`. `description` and `author` are empty strings.

First commit `9025de3` already depended on `aws-sdk`, `body-parser`, `cors`, `express`, `hewer`, `html-pdf`, `memorystream`, and `wkhtmltopdf`, with `aws-sdk` range `^2.999.0`. Later lockfile resolutions and the PhantomJS and PM2 packages are in the tree at HEAD. `eacf1df` is the commit that rewrites most of `package-lock.json`.

## Security

- `cred.json` is in git at HEAD and in history. Treat the keys as exposed. This document does not repeat them. **Requires Confirmation** that they have been rotated and that the IAM user cannot still use them.
- The HTTP API has no authentication.
- HTML is rendered by PhantomJS. Content is fully attacker-controlled if the port is reachable. There is no allow-list of tags or hosts.
- `Math.random` filenames are guessable relative to a UUID. Combined with a public-read object they would be easy to fetch. The public-read ACL is commented out. The bucket policy is not in git, so anonymity of the object is **Unknown**.
- `index_wit SM.js` would place access keys from Secrets Manager into `AWS.config` if `loadFromPath` were a method that accepted an object. As written, it passes an object to a path loader.
- `new Buffer` in `SecretsManager.js` is the deprecated constructor.

No rate limit, helmet, or request authentication middleware is present.

## Development and testing

There is no test file. `npm test` exits 1.

There is no CI configuration in the repository.

The README's testing section is `pm2 log` and CloudWatch. No sample request body is in the README.

A manual call, derived from the handler, would be `POST /htmltopdf` with JSON `{ "html": "<p>...</p>" }` to port 8080. That sentence is **Inferred** from the handler. It was not executed during this documentation pass. The source repository was read only.

## Related Silicon Ark records

These repositories do not name `nodehtmltopdf`. The overlap is recorded so a later reader does not treat it as a fact inside this git history.

| Silicon Ark directory | What it says | Overlap with this repo |
|---|---|---|
| `pmf-backend-ah` | `PaymentReceiptService` posts `{ "html": ... }` to `http://10.106.3.158:8080/htmltopdf`, waits 1.5 seconds, then `GetObject` on `tmt-pmf-prod-invoices` using `filename` | Same path, port, bucket, region, and response field. The IP is not in this repo |
| `paymyfines-backend-new` | Receipt HTML is posted to secret `pmf-pdf-server` field `IP` | This repo's secret id, when used, is `pmf-prod-s3-user`, and it is for S3 keys, not a URL |
| `paymyfines-pdf-application` | Empty git repository. Its own plan says it is not the HTML-to-PDF listener | No code in common |

**Unknown / Requires Confirmation** that `dc92bed` is the code that was, or is, listening on `10.106.3.158:8080`.
