# PRD — lambdaNodejsPdf

Evidence date: 2026-10-10.

Source: `git@bitbucket.org:overdrive/lambdanodejspdf.git`, `master` at `012edf2` (2021-09-30).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `serverless.yml`, `handler.js`, `htmltopdf.js`, `config.js`, `package-lock.json`, and `.serverless/cloudformation-template-update-stack.json`. Provenance is labelled.

The block comment at the top of `htmltopdf.js` is the only written description of an input shape. It is not a product brief. Where that comment and the code disagree, both are recorded.

## Product purpose

**Inferred.** An AWS HTTP API, service name `lambdanodepdf`, that turns a short text query into a PDF and is also written to turn HTML into a PDF stored in S3.

**Implemented.** Two functions in `serverless.yml`: `generate-pdf` and `generate-htmltopdf`.

**Documented** only by the `htmltopdf.js` comment and by commit subjects. There is no README.

**Unknown / Requires Confirmation:** the product owner, the caller, and whether this API is still deployed. The profile name `unity` and the bucket name `pmf-invoices` are not reconciled in this tree. See `project-plan.md`.

## Users and interactions

| Actor | Evidence | Status |
|---|---|---|
| HTTP caller of `GET /api/pdf` | Route in `serverless.yml`. No `authorizer` key | **Implemented**. The caller application is not in this repo |
| HTTP caller of `POST /api/htmltopdf` | Route in `serverless.yml`. No `authorizer` key | **Implemented**. The caller application is not in this repo |
| Person running Serverless on Windows | `.serverless/serverless-state.json` records artifact paths under `C:\wamp64\www\lambdanodejspdf\` | **Implemented** as a packaging path. No deploy script is in git |
| S3 bucket `pmf-invoices` | Literal in `config.js` | **Implemented** as a destination name. The bucket resource is not in this repo |

No user, role, or tenant is represented in the code.

## Systems the product talks to

1. AWS API Gateway HTTP API and Lambda. Service `lambdanodepdf`, runtime `nodejs12.x`, stage `dev`, region `eu-west-1`, profile `unity`. Each function is packaged alone (`package.individually: true`). The committed zips still contain both handlers and the full `node_modules` tree, because `serverless.yml` sets no include or exclude patterns.
2. S3, from `htmltopdf.js` only. `AWS.S3().putObject` to bucket `pmf-invoices`. The execution role in the update template allows CloudWatch Logs and does not allow `s3:PutObject`.
3. The `wkhtmltopdf` binary, spawned by `node_modules/wkhtmltopdf` (`wkhtmltopdf.command = 'wkhtmltopdf'` in the packaged copy of that package). Not present in the zip.
4. CloudWatch log groups `/aws/lambda/lambdanodepdf-dev-generate-pdf` and `/aws/lambda/lambdanodepdf-dev-generate-htmltopdf`. The template creates them and does not set a retention period.

No database, queue, mailer, Secrets Manager, VPC, or monitoring vendor is configured. There is no Bitbucket Pipelines file. `pluginsData` in the Serverless state is empty.

## Major workflows

Routes below are the path in `serverless.yml`. The HTTP API stage in the update template is `$default` with `AutoDeploy: true`. The stack output `HttpApiUrl` is `https://{api-id}.execute-api.{region}.{url-suffix}` with no stage segment. The API id is a CloudFormation `Ref`, not a stored value. **Unknown / Requires Confirmation** of the live URL.

Payload format on both integrations is `2.0`. Integration timeout is 6500 ms. Lambda timeout is 6 seconds.

### WF-1 — Text query to a PDF response

**Implemented** in `handler.js`, `exports.generate_pdf`, `GET /api/pdf`.

Trigger: HTTP GET.

Input: query parameter `text`.

Behaviour:

1. Read `event.queryStringParameters.text`.
2. If that value is missing or empty, use the string `Hello world`.
3. Create a PDFKit document and draw that string with `doc.text`.
4. Collect the PDF bytes, base64-encode them, and resolve an HTTP response.

Output, on the success path:

| Field | Value |
|---|---|
| `statusCode` | `200` |
| `headers["Content-Type"]` | `application/pdf` |
| `body` | base64 PDF |
| `isBase64Encoded` | `true` |

Validation: none beyond the `|| 'Hello world'` fallback. No maximum length. No character filter.

Error conditions **implemented** in the source, with no `catch`:

- If `queryStringParameters` is null, which HTTP API payload 2.0 uses when the request has no query string, the property read throws `TypeError` before the default applies. The handler does not turn that into an HTTP response.
- PDFKit errors reject the promise only if they throw synchronously. The `data`/`end` listeners do not reject.

The handler name in `serverless.yml` is `handler.generate_pdf`. From `c651a1a` until `ee231d5` the YAML said `handler.pdf` while the export was already `generate_pdf`.

Confidence: high for the source behaviour. The committed update template wires this handler to `GET /api/pdf`.

### WF-2 — HTML body to a PDF in S3

**Implemented** in `htmltopdf.js`, `exports.main`, `POST /api/htmltopdf`. Added in `012edf2`.

The file comment says the handler handles events in this shape (spelling in the comment is "eventes"):

```json
{
  "data": "<h1>Claudemiro</h1>",
  "filename": "optional filename",
  "pagesize": "optional pagesize default: a4"
}
```

The code does not read those fields from the Lambda event root. It `JSON.parse`s `event.body` and then reads `data`, `filename`, and `pagesize`. The sample HTML in the comment is an example string, not a stored customer.

Trigger: HTTP POST with a JSON body.

Inputs:

| Field | Required by code | Default |
|---|---|---|
| `data` | Yes. Falsy values take the error branch | none |
| `filename` | No | `Math.random().toString(36).slice(2)` |
| `pagesize` | No | `a4` |

Intended behaviour, as the statements are written:

1. Parse `event.body` as JSON.
2. If `data` is missing, log `unable to get the data` through `hewer` and call `callback('unable to get the data', {})`.
3. Otherwise build `filename = (body.filename or random) + ".pdf"`, take `pagesize` or `a4`, and log `{ data, pageSize, filename }`.
4. Pipe `wkhtmltopdf(data, { pageSize })` into `/tmp/{filename}`.
5. In the `wkhtmltopdf` completion callback, `S3.putObject` that file to bucket `pmf-invoices` with `ContentType: application/pdf` and `Key` equal to `filename`.
6. On S3 error, log and call `callback('Unable to send file to S3', {})`. On success, log and call `callback(null, { filename })`.

What the function actually does when invoked:

- `callback` is not a parameter and is not defined anywhere in the file. Every branch that reaches it throws `ReferenceError`.
- The function is `async` and does not return the work started by `wkhtmltopdf`. After `.pipe(writeStream)` the async function resolves `undefined`. The PDF conversion and the S3 call run in callbacks after that return.
- The value passed to `callback` on the success branch is `{ filename }`, not an API Gateway response (`statusCode`, `headers`, `body`).
- The missing-data and S3-error branches pass a string and `{}`, which is also not an API Gateway response.

So a caller does not receive a PDF body from this route, and does not receive `{ filename }`, unless something outside this file supplies `callback`. Nothing in the repository does.

Further failure conditions in the same function:

- `event.body` null or invalid JSON: `JSON.parse` throws. No `catch`.
- `body` parsed as a non-object: reading `body.data` throws.
- `filename` is concatenated into `/tmp/` and used as the S3 key with no sanitising. A value containing path separators is still used.
- The code always appends `.pdf`, including when `filename` already ends in `.pdf`.
- `pagesize` is not checked against a list. It is passed to the wrapper as `pageSize`, which that package turns into a `--page-size` argument.
- The S3 upload starts in the `wkhtmltopdf` callback and creates a read stream of `/tmp/{filename}`. The code does not wait for the write stream `finish` event. **Inferred:** the read can start before the file is fully flushed.
- The execution role cannot `s3:PutObject`. A call that reached `putObject` would be denied by IAM as this template defines it.
- The zip does not contain a `wkhtmltopdf` binary. `config.js` only adds `LAMBDA_TASK_ROOT` to `PATH`.

`hewer` itself is wired in a way that matches `node_modules/hewer/lib/logger.js` in the artifact: `logger.log(meta)` returns an object with `.info(message)` and `.error(message)`. `logger.log().error('unable to get the data')` and `logger.log({...}).info('Variables')` match that API. The formatter in `config.js` prints `level`, message, and `JSON.stringify(meta)`.

Confidence: high that the handler is incomplete. Confidence that a live stack ever returned a filename: none in this repo.

## Functional requirements

### FR-001 — Expose a text-to-PDF GET route

The service exposes `GET /api/pdf` on an API Gateway HTTP API, handled by `handler.generate_pdf`.

Evidence: `serverless.yml`; `HttpApiRouteGetApiPdf` in the update template (`RouteKey: GET /api/pdf`).

Implementation: **Implemented**.

Confidence: high.

### FR-002 — Default the PDF text

When `queryStringParameters.text` is present and empty, or present and falsy, the PDF text is `Hello world`. When the query object itself is absent, the handler throws.

Evidence: `handler.js`.

Implementation: **Implemented**.

Confidence: high.

### FR-003 — Return the PDF as a base64 HTTP response

The success response is status 200, content type `application/pdf`, body base64, `isBase64Encoded: true`.

Evidence: `handler.js`. The template sets `PayloadFormatVersion: 2.0`, which is the format that honours `isBase64Encoded`. This repo has no captured HTTP transcript.

Implementation: **Implemented** in the handler.

Confidence: high for the response object. Live gateway behaviour: **Requires Confirmation**.

### FR-004 — Expose an HTML-to-PDF POST route

The service exposes `POST /api/htmltopdf`, handled by `htmltopdf.main`, with 1024 MB configured on that function in `serverless.yml`.

Evidence: `serverless.yml`; `HttpApiRoutePostApiHtmltopdf` (`RouteKey: POST /api/htmltopdf`).

Implementation: **Implemented** as a route and a function resource. The handler does not complete the workflow. See WF-2.

Confidence: high.

### FR-005 — Require an HTML `data` field

If the parsed body has no truthy `data`, the handler logs an error and attempts to fail the invocation.

Evidence: `htmltopdf.js`. The failure call uses an undefined `callback`.

Implementation: **Implemented** as a branch that throws `ReferenceError` after the log line.

Confidence: high.

### FR-006 — Name and size the HTML PDF

The S3 key is the supplied `filename` or a random base-36 token, plus `.pdf`. The page size is `pagesize` or `a4`.

Evidence: `htmltopdf.js` and the file comment.

Implementation: **Implemented** as local variables. They are not returned to the caller. See WF-2.

Confidence: high.

### FR-007 — Store the HTML PDF in `pmf-invoices`

The upload target bucket is the literal `pmf-invoices`. Content type is `application/pdf`.

Evidence: `config.js`, `htmltopdf.js`.

Implementation: **Implemented** as arguments to `putObject`. The template does not grant S3 write, and the handler does not reach a successful callback. Whether any object was stored is **Unknown**.

Confidence: high for the intended call. Low for a completed upload.

### FR-008 — Leave the HTTP API unauthenticated

Neither route sets an authorizer. The route resources in the update template set `RouteKey` and `Target` only.

Evidence: `serverless.yml`; `HttpApiRouteGetApiPdf`; `HttpApiRoutePostApiHtmltopdf`.

Implementation: **Implemented**.

Confidence: high for the template. Whether a later console change added auth is **Unknown**.

## Constraints

- Runtime in the template is `nodejs12.x`. No `engines` field exists, because there is no `package.json`.
- Lambda timeout 6 seconds and integration timeout 6500 ms apply to both functions, including HTML rendering.
- Memory in the compiled template is 1024 MB for both functions. `serverless.yml` sets `memorySize: 1024` only on `generate-htmltopdf`. The PDF function's 1024 MB matches the Serverless Framework 2 default that the compiler wrote into the template. **Inferred** that the default, not an explicit YAML key, produced the PDF function's memory.
- No CORS configuration is present on the `HttpApi` resource. Browser preflight is not defined. Non-browser callers are unaffected by that absence.
- Lambda resource policies allow `apigateway.amazonaws.com` to invoke each function for any route on this API (`SourceArn` ending in `/*`).
- HTML in `data` is passed through to `wkhtmltopdf` with no filtering.
- Deployment artifacts, including two zip files of about 10 MB each, are committed. `.npmignore` excludes `.serverless` from npm packs. `.gitignore` does not exclude it from git.
- `sonar-project.properties` and `sonarqube_scan.bat` were listed by the onboarding scan and are not in git.

## Non-requirements

No document in this repository requires accounts, persistence of the generated text PDF, email, a queue, or a retry. Do not treat those as scope.
