Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions .eslintrc.js

This file was deleted.

10 changes: 10 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@ jobs:
run: corepack enable
- name: Install dependencies
run: yarn install --frozen-lockfile --registry=https://registry.npmjs.org/
- name: Audit production dependencies
run: yarn audit:production
- name: Audit full dependency graph
run: yarn audit:all
- name: Build
run: yarn build
- name: Run unit tests
run: yarn test
- name: Inject Sentry source-map metadata
run: yarn sentry:sourcemaps:inject
- name: Package Lambdas
run: yarn lambda:package
- name: Verify Lambda artifact
run: yarn lambda:package:verify
20 changes: 17 additions & 3 deletions .github/workflows/production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,32 @@ jobs:
run: corepack enable
- name: Install dependencies
run: yarn install --frozen-lockfile --registry=https://registry.npmjs.org/
- name: Audit production dependencies
run: yarn audit:production
- name: Audit full dependency graph
run: yarn audit:all
- name: Build
run: yarn build
- name: Run unit tests
run: yarn test
- name: Inject Sentry source-map metadata
run: yarn sentry:sourcemaps:inject
- name: Package Lambdas
run: yarn lambda:package
- name: Verify Lambda artifact
run: yarn lambda:package:verify
- name: Upload Sentry source maps
run: yarn sentry:sourcemaps:upload
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@13d241b293754004c80624b5567555c4a39ffbe3
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy
run: cp serverless-prod.yaml serverless.yaml && yarn sls:deploy && yarn sentry:sourcemaps
- name: Deploy Lambdas sequentially
run: yarn lambda:deploy:prod
- name: Cold-start smoke test Lambdas
run: yarn lambda:smoke:prod
- name: Notify about failure
uses: sarisia/actions-status-discord@v1
if: failure()
Expand Down
20 changes: 17 additions & 3 deletions .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,32 @@ jobs:
run: corepack enable
- name: Install dependencies
run: yarn install --frozen-lockfile --registry=https://registry.npmjs.org/
- name: Audit production dependencies
run: yarn audit:production
- name: Audit full dependency graph
run: yarn audit:all
- name: Build
run: yarn build
- name: Run unit tests
run: yarn test
- name: Inject Sentry source-map metadata
run: yarn sentry:sourcemaps:inject
- name: Package Lambdas
run: yarn lambda:package
- name: Verify Lambda artifact
run: yarn lambda:package:verify
- name: Upload Sentry source maps
run: yarn sentry:sourcemaps:upload
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@13d241b293754004c80624b5567555c4a39ffbe3
uses: aws-actions/configure-aws-credentials@cbe3b392738ccf3f987d68400dafcf4b0624a56c # v6.2.4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Deploy
run: cp serverless-staging.yaml serverless.yaml && yarn sls:deploy && yarn sentry:sourcemaps
- name: Deploy Lambdas sequentially
run: yarn lambda:deploy:staging
- name: Cold-start smoke test Lambdas
run: yarn lambda:smoke:staging
- name: Notify about failure
uses: sarisia/actions-status-discord@v1
if: failure()
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ serverless.yaml
/transfers-data

/.serverless
/.lambda-package

# compiled output
/dist
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ The NestJS codebase lives in `src/`, with feature modules grouped by dependency
- `yarn build` compiles TypeScript to `dist/`.
- `yarn test`, `yarn test:watch`, and `yarn test:cov` run Jest once, in watch mode, or with coverage.
- `yarn lint` and `yarn format` keep imports, spacing, and Prettier rules aligned.
- `yarn new-endpoint` scaffolds a Nest controller/service pair; follow up with module wiring.
- `yarn new-endpoint <name>` scaffolds and wires a Nest controller/service pair.
- `yarn create-migration <name>` generates SQL templates under `migrations/sqls`.
- `yarn audit:production` and `yarn audit:all` enforce the high/critical advisory gates.
- `yarn lambda:package && yarn lambda:package:verify` builds and validates the deployment artifact.

## Coding Style & Naming Conventions
Use TypeScript with 2-space indentation and single quotes (Prettier defaults). Classes, modules, and providers follow Nest conventions (`PascalCase`), while files and functions use `kebab-case` and `camelCase`. Keep DTOs and validators near their consumers in the module folder. Always run `yarn lint` before opening a PR so ESLint/Prettier fixes land in the commit.
Expand All @@ -21,4 +23,4 @@ Jest is configured to discover `.spec.ts` files within `src/`. Name tests after
Write imperative commit subjects under 72 characters (e.g., `Add token pool retry logging`) and bundle related changes together. Reference tickets or incidents in the body when applicable (`B-12345`). Pull requests should include: a high-level summary, testing notes (`yarn test`, `yarn lint`), database migration callouts, and any screenshots of API responses if they clarify the change. Request reviews from domain owners and ensure CI passes before merging.

## Environment & Deployment Notes
For local development, run `sh build-dev-db.sh` followed by `docker-compose up -d` to provision the MariaDB instance (port `3307`, user/password `allowlist`). Add required secrets to `.env.local` as documented in `README.md`. Serverless deployments use the `serverless*.yaml` definitions; coordinate infrastructure changes with DevOps before editing those files.
For local development, run `sh build-dev-db.sh` followed by `docker-compose up -d` to provision the MariaDB instance (port `3307`, user/password `allowlist`). Add required secrets to `.env.local` as documented in `README.md`. Deployment workflows package compiled handlers with frozen production dependencies, then update the existing Lambda functions sequentially through the AWS CLI. The `serverless*.yaml` files document the provisioned infrastructure; coordinate infrastructure changes with DevOps before editing them.
57 changes: 50 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,58 @@ All three managed Lambda functions use the `nodejs24.x` runtime, which is based
on Amazon Linux 2023. Build and package deployments with Node.js 24 so native
dependencies, if introduced, target the same runtime generation.

Deployment remains on Serverless Framework v3 so the existing CI does not gain
the v4 sign-in/license-key requirement. Its configuration validator predates
`nodejs24.x` and emits a non-fatal runtime warning, but the generated
CloudFormation uses `nodejs24.x` for every function.
`yarn build` first uses the Nest compiler to preserve decorator metadata, then
esbuild bundles the resulting entrypoints and their ESM dependencies into
Node.js 24 CommonJS handlers. `yarn lambda:package` creates one reproducible
`.serverless/allowlist-api.zip` from that `dist/` tree, migrations,
configuration, and a frozen
production-only dependency install. The artifact contains no compiler, test,
Serverless Framework, or optimizer dependencies. Run
`yarn lambda:package:verify` before deployment; it checks both handler paths,
loads every handler under the current Node runtime, confirms Sentry debug IDs,
enforces Lambda size limits, and prints the artifact SHA-256.

The checked-in `serverless-*.yaml` files remain as records of the provisioned
infrastructure, but deployment workflows no longer install or run the obsolete
Serverless v3/plugin build chain. They update the existing functions directly
with the AWS CLI, waiting for each function to finish before continuing. The
order is token-pool downloader, allowlist worker, then API, so workers are ready
before the API can enqueue new work. Compatibility handlers under `src/` are
included in the artifact, so changing the configured handlers to `dist/` cannot
create a code/config transition outage.

After staging and production deploys, `yarn lambda:smoke:staging` and
`yarn lambda:smoke:prod` directly invoke all three functions in their respective
regions. The worker events use a reserved, side-effect-free bootstrap probe; the
API probe requests `/api-json`. Each command requires a successful cold-start
`Init Duration`, rejects Lambda function errors and sanitized log evidence of
missing modules, invalid entrypoints, ESM/CommonJS incompatibility, failed
Sentry/bootstrap initialization, or an unsupported Node.js runtime, and validates
each response payload.

Sentry source-map debug IDs are injected before packaging. The matching maps
are uploaded before the Lambda code update, ensuring the deployed JavaScript
and uploaded source maps have the same debug IDs.

New migrations are called on first invocation of any lambda.

In production the app is ran in 3 lambas:

1. API lambda - Serves all API requests (entrypoint: `src/api-lambda.ts/handle`)
2. Worker lambda - Does the actual final allowlist creation (entrypoint: `src/worker-lambda.ts/handle`)
3. Tokenpool downloader lambda - Helps to get aggregated tokenpool data needed for worker lambda (entrypoint: `src/token-pool-downloader-lambda.ts/handle`)
1. API lambda - Serves all API requests (handler: `dist/api-lambda.handler`)
2. Worker lambda - Does the actual final allowlist creation (handler: `dist/worker-lambda.handler`)
3. Tokenpool downloader lambda - Helps to get aggregated tokenpool data needed for worker lambda (handler: `dist/token-pool-downloader-lambda.handler`)

## Dependency security policy

CI and both deployment workflows run two independent audits:

- `yarn audit:production` checks the deployable dependency graph.
- `yarn audit:all` checks production and development dependencies.

Both commands fail on any unapproved high or critical advisory, fail closed if
the registry returns no audit result, and print each remaining advisory with
its dependency path. Lower-severity findings remain visible but do not block.
If an advisory truly cannot be removed immediately, add only its exact advisory
ID and dependency path to `dependency-audit-exceptions.json`, together with a
reason and ISO expiry date. Broad package exceptions are not supported. The
exception file is intentionally empty at present.
1 change: 1 addition & 0 deletions dependency-audit-exceptions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
26 changes: 26 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import eslint from '@eslint/js';
import prettierRecommended from 'eslint-plugin-prettier/recommended';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{
ignores: ['dist/**', 'coverage/**', '.serverless/**'],
},
eslint.configs.recommended,
...tseslint.configs.recommended,
prettierRecommended,
{
files: ['**/*.ts'],
languageOptions: {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
);
71 changes: 53 additions & 18 deletions new-api-endpoint-generator-wrapper.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,59 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { exec } = require('child_process');
const controllerName = process.argv[2];
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs');
const { join } = require('node:path');

if (!controllerName) {
console.error('Please provide an controller name.');
const requestedName = process.argv[2];
if (!requestedName || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(requestedName)) {
console.error('Provide a controller name containing only letters, digits, _ or -.');
process.exit(1);
}

const hygenCommand = `hygen api-endpoint new --controllerName ${controllerName}`;
exec(hygenCommand, (error, stdout, stderr) => {
if (error) {
console.error(`Error executing hygen command: ${error.message}`);
console.error(`Error details: ${error}`);
return;
}
const words = requestedName
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.split(/[-_\s]+/)
.filter(Boolean)
.map((word) => word.toLowerCase());
const className = words
.map((word) => word[0].toUpperCase() + word.slice(1))
.join('');
const variableName = className[0].toLowerCase() + className.slice(1);
const pathName = words.join('-');
const endpointDirectory = join('src', 'api', pathName);
const controllerFile = join(endpointDirectory, `${pathName}.controller.ts`);
const serviceFile = join(endpointDirectory, `${pathName}.service.ts`);

if (stderr) {
console.error(`Error: ${stderr}`);
return;
}
if (existsSync(controllerFile) || existsSync(serviceFile)) {
throw new Error(`Endpoint ${pathName} already exists.`);
}

console.log(`Output: ${stdout}`);
});
mkdirSync(join(endpointDirectory, 'models'), { recursive: true });
writeFileSync(join(endpointDirectory, 'models', '.gitkeep'), '');
writeFileSync(
controllerFile,
`import { Controller } from '@nestjs/common';\nimport { ${className}Service } from './${pathName}.service';\n\n@Controller('your-controller-endpoint/${pathName}')\nexport class ${className}Controller {\n constructor(private readonly ${variableName}Service: ${className}Service) {}\n}\n`,
);
writeFileSync(
serviceFile,
`import { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class ${className}Service {}\n`,
);

const apiModulePath = join('src', 'api', 'api.module.ts');
let apiModule = readFileSync(apiModulePath, 'utf8');
const injections = [
[
'// Placeholder for future imports',
`import { ${className}Service } from './${pathName}/${pathName}.service';\nimport { ${className}Controller } from './${pathName}/${pathName}.controller';\n`,
],
[
' // Placeholder for future controllers',
` ${className}Controller,\n`,
],
[' // Placeholder for future services', ` ${className}Service,\n`],
];
for (const [marker, insertion] of injections) {
if (!apiModule.includes(marker)) {
throw new Error(`Missing ApiModule generator marker: ${marker}`);
}
apiModule = apiModule.replace(marker, `${insertion}${marker}`);
}
writeFileSync(apiModulePath, apiModule);
console.log(`Created ${className}Controller and ${className}Service.`);
Loading