# PRD — unityNodejsApi

Evidence date: 2026-09-30.

Source: `git@bitbucket.org:overdrive/unitynodejsapi.git`, `master` at `8d9588c` (2019-12-01).

There is no prior PRD in the source repository or in Silicon Ark. Requirements below are recovered from `index.js`, `login.js`, `test.js`, `bitbucket-pipelines.yml`, and the five commits on that revision. Provenance is labelled. Database password, JWT signing secret, and the hardcoded username and password in `test.js` are omitted.

## Product purpose

**Inferred.** A Node.js AWS Lambda function, named `unityAuth` by the pipeline, that accepts an event with `module` and `action` and is intended to log a person in against a MySQL `users` table, check a token, or refresh an access token.

**Implemented.** `index.js` only branches for `module === "auth"`. The login function returns before its query callback finishes. `verify` is an empty body. `getAccessToken` cannot read the JWT config that `loginUser` declares.

**Documented** only by commit subjects ("New mysql module", "Update the hash md5", "Adding new Mysql2 module for node") and by comments inside the code. There is no README.

**Unknown / Requires Confirmation:** the product owner, the caller, and whether this function was replaced by the Cognito flow documented for `unity-skeleton`.

## Users and interactions

| Actor | Evidence | Status |
|---|---|---|
| Caller sending a Lambda event | `exports.handler = async (event) =>` in `index.js` | **Implemented**. The caller is not named |
| Person identified by `strUsername` | SQL filters `users.strUsername` and compares `strPassword` | **Implemented** as a column. No screen is in this repo |
| Role on the row (`strRole`) | Selected and aliased `role`. No branch reads it | **Implemented** as data returned on the user object inside the query callback. Not an authorisation check |
| Operator of Bitbucket Pipelines | `bitbucket-pipelines.yml` default pipeline | **Implemented** as YAML. Who may run it is **Unknown** |
| Person running `node test.js` | `test.js` connects on load | **Implemented** script. Not referenced by the handler |

No session store, cookie, role check, or user-admin screen exists in this repository.

## Systems the product talks to

1. AWS Lambda function `unityAuth` in region `us-east-2`. The pipeline updates its zip. The function resource itself is not in git.
2. MySQL on host `unityqa.csigt2kcstpl.us-east-2.rds.amazonaws.com`, database `unityqa`, user `dbadmin`. Password is in `login.js` and `test.js` and is omitted here. Driver at HEAD is `mysql2` (`createPool`).
3. In-process JWT (`jsonwebtoken`) using a secret string in source (omitted) and `expiresIn: "2 days"`.
4. Bitbucket Pipelines, image `node:10.15.3`, pipe `atlassian/aws-lambda-deploy:0.2.1`. AWS keys are pipeline variables, not files.

No HTTP API client, queue, mailer, or file store is present. The JWKS client and API Gateway policy document existed only in `login.js` before `d4602f7`.

## Major workflows

### WF-1 — Pipeline packages and updates Lambda

**Implemented** in `bitbucket-pipelines.yml`:

1. Default pipeline. No branch filter. No test step.
2. Image `node:10.15.3` (was `nodejs:10.x` at `e367364`, changed the same night in `bd7d5af`).
3. Step "Build and package": `apt-get update && apt-get install -y zip`, then `zip -r codeNodejs.zip *`. The artifact is `codeNodejs.zip`.
4. Step "Update Lambda code": pipe command `update`, region `us-east-2`, `FUNCTION_NAME` `unityAuth`, `ZIP_FILE` `codeNodejs.zip`.
5. Shell `*` does not expand dotfiles, so `.git` is not in the zip. `node_modules` is not a dotfile and is tracked, so it is in the zip. There is no `npm ci`.

Whether a pipeline has succeeded, and whether the Lambda handler property is `index.handler`, is **Unknown / Requires Confirmation**.

### WF-2 — Handler dispatch

**Implemented** in `index.js`:

1. `var login = require('./login')`. `var result = ""` at module scope.
2. `exports.handler` is `async` and takes only `event`. It does not take `context` or a callback.
3. `switch (event.module)` case `"auth"`:
   - `action == 'login'` assigns `login.loginUser(event.email, event.password)`.
   - `action == 'verify'` assigns `login.verify(event.token)`.
   - `action == 'access-token'` assigns `login.getAccessToken(event.access_token)`.
4. The three `if` statements are not `else if`. The action strings differ, so only one matches.
5. `default` evaluates `context.fail(...)`. `context` is not declared and is not a parameter. That throws `ReferenceError` before `fail` is called. The handler does not catch it.
6. If the switch does not throw, the handler returns `{ statusCode: 200, body: JSON.stringify(result) }`. It does not read a status from the tuple that login functions return.

If `event` is missing, `event.module` throws before the switch. There is no guard.

### WF-3 — Login

**Implemented** in `login.loginUser`. The handler treats the return value as the HTTP body. The function does not return the query result to that caller.

1. Every call creates a new `mysql2` pool to the RDS host, user `dbadmin`, database `unityqa`.
2. `jwtConfig` is local to this function: secret (omitted) and `expiresIn: "2 days"`.
3. An error object is created immediately: `email: 'Check your username/email'`, `password: 'Check your password'`.
4. `pool.getConnection` schedules a callback. The `err` argument is not checked.
5. The function then `return [200, error]` with that initial error object.
6. Inside the callback, the code hashes `passwordin`. `login.js` does not declare `passwordin`. The parameter is `password`, and a later `var password = ""` shadows it. `passwordin` exists only in `test.js`. The callback throws `ReferenceError` before the query.
7. If `passwordin` were defined, the callback would MD5 it (`crypto.createHash('md5').update(...).digest("hex")`), `mysql.escape` the email and the digest, and query `users`.
8. The query selects `strUuid as uuid`, `strUsername as email`, `strPassword as passwordq`, `strRole as role`, `txtPreference as data`, filtered by username and password digest.
9. `results[0]` becomes `user`. A second error object sets `email` null when `user` is truthy, and `password` null when `user` exists and `password === user.passwordq`.
10. On no error strings, it `delete user['password']` (the selected alias is `passwordq`), signs `{ id: user.uuid }`, and `return [200, { user, access_token }]` from inside the callback. That return does not become the return value of `loginUser`.

`//context.callbackWaitsForEmptyEventLoop = false` is commented out. The pool is never closed. What the Lambda platform does with the open handles after the handler has already returned is **Unknown / Requires Confirmation**.

### WF-4 — Verify

**Implemented** in `login.verify`. The body is the comment `// func2 impl`. It takes no parameters. `index.js` still passes `event.token`. The call returns `undefined`. `JSON.stringify(undefined)` returns `undefined`, so the Lambda `body` property is not a JSON string.

The removed authorizer (WF-6) was the only token-verification implementation in git history.

### WF-5 — Access token

**Implemented** in `login.getAccessToken(access_token)`:

1. `try` calls `jwt.verify(access_token, jwtConfig.secret)`. `jwtConfig` is declared inside `loginUser`, not here, and not at module scope.
2. That reference throws. The `catch` returns `[401, { error: "Invalid access token detected" }]`.
3. The lines after a successful verify are therefore not reached: a commented `_.cloneDeep(authDB.users.find(...))`, `delete user['password']`, and a new `jwt.sign`. `user`, `_`, and `authDB` are not defined in this function. At `21214d8` the `_.cloneDeep` line was not commented out, and `_` and `authDB` were still absent.
4. `index.js` still wraps this return value in `statusCode: 200`. The 401 is only inside the JSON body.

### WF-6 — Removed API Gateway authorizer

**Removed** at `d4602f7`. Present in `login.js` at `e367364` and `bd7d5af`:

1. `login.js` exported its own `handler(event, context, callback)`, separate from `index.js`.
2. It built a `jwks-rsa` client from `process.env.JWKS_URI` (cache 86400000 ms, 10 requests per minute, `strictSsl: true`).
3. It read a Bearer token from `event.authorizationToken`.
4. It called `jwt.verify` with algorithms set to the string `"RS256"` and a `getSigningKey` helper. `jsonwebtoken` expects `algorithms` to be an array. The helper's default parameter referenced `decoded`, which the file did not declare.
5. Success invoked the Lambda callback with an IAM `Allow` on `execute-api:Invoke` for `process.env.RESOURCE` and `principalId: "user"`. Failure called back `"Unauthorized"`.
6. `jwks-rsa` is not in `package-lock.json` or in `node_modules` in any commit. Loading that file would throw on `require('jwks-rsa')`.

`index.js` at that time did not require `login.js`. It returned `event.key1` with status 200.

## Functional requirements recovered from the implementation

These describe what the code does at `8d9588c`. They are not an approved specification.

### FR-001 — Auth module dispatch

The handler runs login, verify, or access-token only when `event.module` is the string `"auth"` and `event.action` is `login`, `verify`, or `access-token`.

Evidence: `index.js` switch. **Implemented.**

### FR-002 — Unknown module fails the invocation

Any other `event.module` reaches `default` and throws because `context` is not defined.

Evidence: `index.js` default arm. **Implemented** as a throw. The commit message does not say this was intentional.

### FR-003 — HTTP status on the Lambda response is 200 whenever the handler returns

The returned object sets `statusCode: 200`. Login's `[200, ...]` and access-token's `[401, ...]` are serialized into `body`. They do not set the Lambda status code.

Evidence: `index.js` response object. **Implemented.**

### FR-004 — Login compares an MD5 digest to `users.strPassword`

The query callback hashes with MD5 and matches `strUsername` and `strPassword`.

Evidence: `login.js` query and commit `21214d8` "Update the hash md5". **Implemented** inside the callback. On the path `index.js` calls, the callback references `passwordin` and throws before the query. **Requires Confirmation** whether the live function was ever edited outside git.

### FR-005 — A successful login signs a JWT

The payload is `{ id: user.uuid }` where `uuid` is `strUuid`. Expiry is the string `"2 days"`. The secret is a literal in source.

Evidence: `jwt.sign` in `login.js`. **Implemented** in the callback, which the handler does not await. Secret omitted.

### FR-006 — Login reports a fixed pair of messages when it returns to the handler

The synchronous return is `[200, { email: 'Check your username/email', password: 'Check your password' }]`. The later, more precise error object (null email when a row exists) is local to the callback.

Evidence: the `return` after `pool.getConnection` in `login.js`. **Implemented.**

### FR-007 — Verify does not check a token

`verify` has no implementation. The token argument is dropped.

Evidence: `login.js` `verify`. **Implemented** stub. The name is the only clue that a check was intended. **Inferred** intent. **Requires Confirmation.**

### FR-008 — Access-token refresh returns an invalid-token error

`getAccessToken` returns `[401, { error: "Invalid access token detected" }]` from `catch` because `jwtConfig` is out of scope. It does not load a user and does not issue a new token.

Evidence: `login.js` `getAccessToken`. **Implemented.**

### FR-009 — The user row shape the query expects

Selected columns: `strUuid`, `strUsername`, `strPassword`, `strRole`, `txtPreference`. Aliases: `uuid`, `email`, `passwordq`, `role`, `data`.

Evidence: SQL string in `login.js` and the same string in `test.js`. At `21214d8` the select was `userID as uuid`, `strUsername as email`, `strPassword as password` (no role, no preference). The table definition is not in the repository. **Requires Confirmation** of the live columns.

### FR-010 — Password digest is not removed under the name the query uses

On the success path the code deletes `user.password`. The column alias is `passwordq`. The digest field is not the property `delete` names.

Evidence: `delete user['password']` in `login.js`. **Implemented.** Whether leaving `passwordq` on the object was intended is **Unknown / Requires Confirmation.**

### FR-011 — Deploy is an in-place Lambda code update

A push that runs the default pipeline zips the tree and updates function `unityAuth`. It does not create the function, set environment variables, or run tests.

Evidence: `bitbucket-pipelines.yml`. **Implemented** as pipeline definition.

### FR-012 — Manual script

`node test.js` (or any load of that file) opens a pool and runs the login query with a username and password written in the file. It has no assertions and is not called from `index.js`.

Evidence: `test.js`, added in `8d9588c`. Credential values omitted. **Implemented.**

## Constraints

- Node image pin `10.15.3` in the pipeline. Node 10 is what the YAML requests. The Lambda runtime is not declared in git. **Unknown / Requires Confirmation.**
- JWT lifetime string `"2 days"`. A comment in `login.js` says a numeric value would mean seconds and a string must include units. **Implemented** config, explained by that comment.
- SQL at HEAD uses `mysql.escape`. The previous revision concatenated the email and digest inside quotes (`21214d8`). **Implemented** change in `8d9588c`.
- Secrets are source literals, not environment variables, from `d4602f7` onward. The removed authorizer did use `process.env.JWKS_URI` and `process.env.RESOURCE`.

## Discrepancies

| Topic | One side | Other side |
|---|---|---|
| Login result | Handler assigns the return value of `loginUser` | `loginUser` returns the error tuple before the query callback |
| Password input | `loginUser(user_email, password)` | Callback hashes `passwordin`, defined only in `test.js` |
| Token verify | Action `verify` is wired | Function body is empty. The JWKS authorizer that did verify was deleted |
| HTTP 401 | `getAccessToken` catch returns 401 in a tuple | Handler status code is 200 |
| `package.json` | Lockfile and `node_modules` are present | No manifest in any commit |
| `jwks-rsa` | Required by the first `login.js` | Never committed under `node_modules` |
| Scan listing | `sonar-project.properties`, `sonarqube_scan.bat` | Not in git |
| Unity family | Repository and Lambda names use "unity" | No reference to the other Unity trees in Silicon Ark |
