# Technical documentation — lambdaNodejsPdf

Evidence date: 2026-10-10.

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

No passwords, access keys, or API ids are stored in the source files. The CloudFormation template uses `Ref` and `Fn::Sub` for account and API id. Those values are not copied here. The Serverless state file records a local Windows path; it does not record AWS secret keys.

## What this repository is

A Serverless Framework v2 service (`service: lambdanodepdf`) that packages two Node.js 12 Lambda functions behind an API Gateway HTTP API. One function builds a PDF with PDFKit and returns it. The other is written to render HTML with `wkhtmltopdf` and upload the file to S3.

There is no `README`, no `package.json`, and no test suite. The lockfile and the handlers are the dependency record. Deployment output under `.serverless/` is committed, including the zip artifacts.

## Repository layout

```text
serverless.yml                                          service, provider, two functions
handler.js                                              generate_pdf
htmltopdf.js                                            main
config.js                                               bucket, PATH, hewer logger
package-lock.json                                       lockfileVersion 1, no package.json
.npmignore                                              node_modules, jspm_packages, .serverless
.gitignore                                              stock Atlassian template; does not list .serverless
.serverless/serverless-state.json                       framework state after a local package
.serverless/cloudformation-template-create-stack.json   template from the PDF-only package
.serverless/cloudformation-template-update-stack.json   template with both functions
.serverless/generate-pdf.zip                            function artifact
.serverless/generate-htmltopdf.zip                      function artifact
```

Twelve tracked paths. `node_modules/` is gitignored and is present inside the zips.

The onboarding scan also listed `sonar-project.properties` and `sonarqube_scan.bat`. They are not in git history.

## Runtime and framework

| Setting | Value | Where |
|---|---|---|
| Service | `lambdanodepdf` | `serverless.yml` |
| Framework | `frameworkVersion: '2'` | `serverless.yml` |
| Provider | `aws` | `serverless.yml` |
| Runtime | `nodejs12.x` | `serverless.yml` and both `AWS::Lambda::Function` resources |
| Region | `eu-west-1` | `serverless.yml` |
| Stage | `dev` | `serverless.yml` |
| AWS CLI profile | `unity` | `serverless.yml` |
| Package | `individually: true` | `serverless.yml` |
| Function versioning | `versionFunctions: true` in Serverless state | compiled versions have `DeletionPolicy: Retain` |
| VPC | `vpc: {}` | Serverless state. No subnet or security group |

`nodejs12.x` is the runtime pinned in git. AWS later retired that Lambda runtime. Whether this stack can still be updated is **Unknown / Requires Confirmation**.

## Functions

| YAML key | Handler | Lambda name | Route | Memory in YAML | Memory in template | Timeout in template |
|---|---|---|---|---|---|---|
| `generate-pdf` | `handler.generate_pdf` | `lambdanodepdf-dev-generate-pdf` | `GET /api/pdf` | not set | 1024 | 6 |
| `generate-htmltopdf` | `htmltopdf.main` | `lambdanodepdf-dev-generate-htmltopdf` | `POST /api/htmltopdf` | 1024 | 1024 | 6 |

No environment variables are set on either function. No layers. No reserved concurrency. No dead-letter queue.

The PDF function's memory and both timeouts are present in the compiled template and absent from `serverless.yml`. They match Serverless Framework 2's defaults (1024 MB, 6 seconds). **Inferred** that the compiler filled them in. The HTML function's memory is explicit in YAML.

Both versions in the update template share `CodeSha256` `Ay/g36TLP5lMvdA+YKx1CDoi+S9qZNpZ3KBwg2W7qQg=`. Listing the zips shows the same 4554 entries and the same project files in each. `individually: true` without include/exclude patterns still packs the whole tree into each zip.

Artifact key prefix in the update template:

```text
serverless/lambdanodepdf/dev/1632951848497-2021-09-29T21:44:08.497Z/
```

Serverless state records the local zip paths as `C:\wamp64\www\lambdanodejspdf\.serverless\generate-pdf.zip` and the matching `generate-htmltopdf.zip`. That is the packaging machine, not the Lambda path.

## HTTP API

Resource `HttpApi`, type `AWS::ApiGatewayV2::Api`:

| Property | Value |
|---|---|
| Name | `dev-lambdanodepdf` |
| Protocol | `HTTP` |
| Stage | `$default` |
| AutoDeploy | `true` |
| Detailed metrics | `false` |
| CORS | not set |
| Authorizer | not set |

Integrations are `AWS_PROXY`, `PayloadFormatVersion: 2.0`, `TimeoutInMillis: 6500`.

| Route key | Integration target |
|---|---|
| `GET /api/pdf` | `GenerateDashpdfLambdaFunction` |
| `POST /api/htmltopdf` | `GenerateDashhtmltopdfLambdaFunction` |

Stack output `HttpApiUrl` joins `https://`, the API id, `.execute-api.`, the region, and the AWS URL suffix. No stage path. The id itself is not materialised in git.

Lambda permissions allow `lambda:InvokeFunction` from `apigateway.amazonaws.com` for `arn:...:execute-api:{region}:{account}:{api-id}/*`.

Payload 2.0 consequences visible from the handlers:

- `handler.js` reads `event.queryStringParameters`. When the client sends no query string, that property is null and the handler throws.
- `htmltopdf.js` reads `event.body` as a JSON string. Payload 2.0 delivers `body` as a string, base64-decoded by the runtime when `isBase64Encoded` is false. The handler does not check `isBase64Encoded` before `JSON.parse`.

## Handler details

### `handler.generate_pdf`

```text
queryStringParameters.text  ->  PDFKit document  ->  buffer  ->  base64 response
```

Dependencies: `pdfkit` only. No AWS SDK. The response object is described in `prd.md` FR-003.

The promise passed to the Lambda runtime resolves only from the PDFKit `end` listener. It has no `reject` path.

### `htmltopdf.main`

```text
JSON body -> wkhtmltopdf(data, { pageSize }) -> /tmp/{filename}
          -> S3.putObject (bucket pmf-invoices) -> callback (undefined)
```

`config.js` is required first and mutates `process.env.PATH`:

```text
PATH = PATH + ":" + LAMBDA_TASK_ROOT
```

The separator is `:`, including on a Windows packaging machine. Lambda's Amazon Linux environment uses `:`. **Inferred** that this line is aimed at the Lambda task root, not at the WAMP working copy.

`new AWS.S3()` is called with no options. Region, credentials, and endpoint come from the SDK's default chain. No region is set in code. The provider region is `eu-west-1`.

`callback` is referenced and never declared. See `business-rules.md` BR-012.

The `wkhtmltopdf` package in the zip (`node_modules/wkhtmltopdf/index.js`) sets `wkhtmltopdf.command = 'wkhtmltopdf'` and `spawn`s it. Files in that package directory are `index.js`, `package.json`, and `README.md`. There is no binary at the zip root. A lookup on `PATH` inside Lambda will not find `wkhtmltopdf` from this artifact.

## Dependencies

There is no `package.json`. `package-lock.json` is lockfileVersion 1 (`requires: true`). Direct `require()` calls and the versions present in that lockfile:

| Module | Version in lockfile | Required from | Notes |
|---|---|---|---|
| `pdfkit` | 0.12.3 | `handler.js` | Also pulls `fontkit` 1.8.1 |
| `wkhtmltopdf` | 0.4.0 | `htmltopdf.js` | Spawns the `wkhtmltopdf` binary. Does not vendor it |
| `hewer` | 0.4.0 | `config.js` | Logger. `log(meta)` returns `.info` / `.error` |
| `aws-sdk` | not in the lockfile | `htmltopdf.js` | Not in the zip's top-level packages. Node.js 12 Lambda included AWS SDK v2 |

`npm ci` cannot be run from this tree, because npm expects `package.json`. The zips are the only committed install result. They were built where Serverless state says the project lived (`C:\wamp64\www\lambdanodejspdf`).

`hewer`'s `Logger` is constructed as `new hewer.Logger(null, null, formatter)`. In the packaged `lib/logger.js`, null filters become the identity filter and null writers become a console writer. The formatter in `config.js` is `` `${level} ${message} ${JSON.stringify(meta)}` ``.

## IAM and security

Role logical id `IamRoleLambdaExecution`. Role name pattern:

```text
lambdanodepdf-dev-{AWS::Region}-lambdaRole
```

Assume-role principal: `lambda.amazonaws.com`.

Inline policy name pattern `lambdanodepdf-dev-lambda`. Two statements:

| Action | Resource |
|---|---|
| `logs:CreateLogStream`, `logs:CreateLogGroup` | `arn:{partition}:logs:{region}:{account}:log-group:/aws/lambda/lambdanodepdf-dev*:*` |
| `logs:PutLogEvents` | `arn:{partition}:logs:{region}:{account}:log-group:/aws/lambda/lambdanodepdf-dev*:*:*` |

No `s3:*` on `pmf-invoices`. No `lambda:InvokeFunction` for other functions. The deployment bucket is a separate Serverless-managed bucket with a policy that denies `s3:*` when `aws:SecureTransport` is false. That bucket is for CloudFormation artifacts, not for invoice PDFs.

Other gaps visible in source, not a pentest:

- Both routes are unauthenticated in the template.
- `filename` is interpolated into a filesystem path and an S3 key.
- `data` is passed to `wkhtmltopdf` unfiltered.
- `htmltopdf.js` logs the HTML (`data`) through `hewer` at info level.
- The S3 callback always `console.error`s its error argument, including the success case where that argument is null.
- Committed zips make the packaged `node_modules` part of the git history. No `.env` or key file is in the source tree.

## Data

No database and no table. The only stored object the code attempts to create is an S3 object:

| Attribute | Value |
|---|---|
| Bucket | `pmf-invoices` |
| Key | `(filename or random) + ".pdf"` |
| Body | read stream of `/tmp/{key}` |
| ContentType | `application/pdf` |
| ACL | not set |

The text-PDF route stores nothing. The response is the document.

## Logging and monitoring

Log groups are created by the template. No retention, no subscription, no alarm, no X-Ray, no Serverless Dashboard flag (`isDashboardMonitoringPreconfigured: false`).

`handler.js` does not log.

## Deployment

The only deployment description in git is the committed Serverless output.

| File | What it is |
|---|---|
| `cloudformation-template-create-stack.json` | Create template from the PDF-only package (`c651a1a`). It does not contain `generate-htmltopdf` |
| `cloudformation-template-update-stack.json` | Update template at `012edf2`, both functions |
| `serverless-state.json` | Framework state aligned with the update |

No `serverless deploy` script, no pipeline, no Makefile. Profile `unity` means whoever deployed used that AWS CLI profile name. Which account that profile selected is not in git.

`.npmignore` excludes `.serverless` when npm packs the directory. Git still tracks `.serverless` because `.gitignore` does not list it.

## Local development and tests

Not documented in the repository. There is no `serverless-offline` plugin (`pluginsData` is empty). There is no Jest or Mocha config, and no `test` script, because there is no `package.json`.

A local invoke would need Node, the Serverless CLI, the `unity` AWS profile for a real deploy, a `wkhtmltopdf` binary on `PATH` for the HTML route, and an S3 client that can write `pmf-invoices`. None of that is scripted here.

## Configuration

| Name | Kind | Value in git |
|---|---|---|
| `bucket` | exported constant | `pmf-invoices` |
| `logger` | exported `hewer` logger | constructed in `config.js` |
| `PATH` | mutated `process.env` | existing `PATH` plus `LAMBDA_TASK_ROOT` |
| `LAMBDA_TASK_ROOT` | Lambda runtime env | not set in the template; Lambda sets it |
| AWS credentials | SDK default chain | not in git |
| CLI profile | Serverless provider | `unity` |

No `.env` file is tracked. `.gitignore` does not mention `.env`.

## Auth

No application auth. See BR-001. The Lambda execution role is the AWS identity of the function, not of the caller. The caller of the HTTP API is anonymous as this template is written.

## Background work, queues, storage

No SQS, SNS, EventBridge, or worker process. The HTML route's PDF conversion is in-process and callback-based, inside the 6-second Lambda invocation. Storage attempted: S3 `pmf-invoices` and `/tmp` on the Lambda environment. `/tmp` is not durable.

## Testing process

None in the repository. No CI configuration. Confidence in runtime behaviour of WF-2 is from reading the handler, not from a test run. WF-1 is small enough that the success response object is fully visible in `handler.js`; it was not executed during this documentation pass.
