Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f369b15
feat(rate-limiting): define IRateLimitStrategy interface
maxx-GENIUS Oct 10, 2026
4d0345f
feat(rate-limiting): extract MemoryRateLimitStrategy
maxx-GENIUS Oct 10, 2026
524956d
feat(rate-limiting): extract RedisRateLimitStrategy with safe guards
maxx-GENIUS Oct 10, 2026
04191fe
feat(rate-limiting): extract ViolationsService
maxx-GENIUS Oct 10, 2026
49dd0a0
feat(rate-limiting): extract PolicyService for whitelist/blacklist rules
maxx-GENIUS Oct 10, 2026
65e6427
refactor(rate-limiting): modularize RateLimiterService coordinator (<…
maxx-GENIUS Oct 10, 2026
7fc5c3f
feat(rate-limiting): register decomposed strategy and policy collabor…
maxx-GENIUS Oct 10, 2026
a0c3a2a
feat(rate-limiting): export new strategies and collaborators from index
maxx-GENIUS Oct 10, 2026
abd6398
fix(types): export RateLimitState as RateLimitStateType in barrel export
maxx-GENIUS Oct 10, 2026
d277868
fix: bind authenticated user, add MIME type filter, and fix CI artifa…
memplethee-lab Oct 10, 2026
177b29f
fix(types): resolve TypeScript compilation errors
memplethee-lab Oct 10, 2026
717eb6c
fix(types): use non-null assertion for file in fileFilter after guard
memplethee-lab Oct 10, 2026
8b37637
fix(types): extract file to local const for proper type narrowing
memplethee-lab Oct 10, 2026
0f7e24f
feat(health): define explicit ComponentStatus and HealthStatus enums …
syntaxsuuvam-spec Oct 10, 2026
b32ac15
docs(probes): document ComponentStatus and HealthStatus enum contract…
syntaxsuuvam-spec Oct 10, 2026
8369bbf
Merge pull request #298 from syntaxsuuvam-spec/feat/issue-288-health-…
memplethee-lab Oct 10, 2026
843ea52
Merge pull request #297 from memplethee-lab/feature/file-upload-secur…
memplethee-lab Oct 12, 2026
224a6bd
feat(common): add CSPRNG-backed random id helpers
memplethee-lab Oct 12, 2026
fd1f476
fix(rate-limiting): derive violation ids and bucket request keys from…
memplethee-lab Oct 12, 2026
e2f83e2
fix(profiling): use crypto randomUUID for profile, snapshot and reque…
memplethee-lab Oct 12, 2026
6f61e7f
fix(observability): use crypto randomUUID for request-timing correlat…
memplethee-lab Oct 12, 2026
af82ae2
test(common): guard against Math.random() reappearing in id generation
memplethee-lab Oct 12, 2026
026114d
fix(auth): pin JWT algorithm allowlist and centralise token lifetimes
ykargeee-bit Oct 12, 2026
2c87a98
fix(auth): pass an explicit expiresIn when EnhancedAuthService issues…
ykargeee-bit Oct 12, 2026
be02f2d
test(auth): assert access-token TTL, expiry enforcement and algorithm…
ykargeee-bit Oct 12, 2026
7168a0e
docs(auth): document access/refresh token lifetimes and the upgrade i…
ykargeee-bit Oct 12, 2026
2aca2e2
test(jest): relocate the portfolio and oracle specs out of test/
ykargeee-bit Oct 12, 2026
7bf9213
test(jest): relocate test/recovery.spec.ts to src/core/auth/
ykargeee-bit Oct 12, 2026
6f45a71
test(jest): relocate and repair test/email-linking.spec.ts
ykargeee-bit Oct 12, 2026
7c40dd0
test(jest): relocate and repair test/wallet-auth.spec.ts
ykargeee-bit Oct 12, 2026
ac43354
test(auth): drop the dead test/enhanced-auth.service.spec.ts and cove…
ykargeee-bit Oct 12, 2026
20e4f3a
test(jest): rename test/oracle-e2e.spec.ts to the .e2e-spec.ts conven…
ykargeee-bit Oct 12, 2026
49bf40a
test(jest): guard against unit specs that no Jest project will run
ykargeee-bit Oct 12, 2026
265c9d7
fix(auth,tests): type the JWT config against JwtModuleOptions and use…
ykargeee-bit Oct 12, 2026
7053ef1
test(email-linking): type the repository mocks so the create() assert…
ykargeee-bit Oct 12, 2026
179f277
test(jest): drop test/portfolio/portfolio.service.spec.ts rather than…
ykargeee-bit Oct 12, 2026
20e0260
test(rate-limiting): annotate the violation fixture so its literal ty…
memplethee-lab Oct 12, 2026
95417db
fix(file-upload): enforce the MIME allowlist in the handler, not a de…
memplethee-lab Oct 12, 2026
55baec6
fix(file-upload): enforce the MIME allowlist in the handler, not a de…
ykargeee-bit Oct 12, 2026
5c0cbf7
Merge pull request #299 from memplethee-lab/feature/crypto-random-ids
memplethee-lab Oct 12, 2026
3e4b769
Merge pull request #300 from ykargeee-bit/feature/auth-token-ttl-and-…
memplethee-lab Oct 12, 2026
6976534
refactor(rate-limiting): sync with main and adopt CSPRNG random-id utils
maxx-GENIUS Oct 14, 2026
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ FILE_CLEANUP_ENABLED=true
FILE_ORPHAN_RETENTION_DAYS=7
FILE_EXPIRED_RETENTION_DAYS=1
FILE_DEFAULT_EXPIRY_HOURS=720
# Comma-separated allowlist of MIME types accepted at the upload boundary.
# Requests with any other MIME type are rejected with 400 before storage.
# Default covers images, documents, video, audio and archives.
FILE_ALLOWED_MIME_TYPES=image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/tiff,image/bmp,image/avif,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,text/plain,text/csv,application/json,application/xml,video/mp4,video/webm,video/ogg,video/quicktime,video/x-msvideo,video/x-matroska,audio/mpeg,audio/wav,audio/ogg,audio/webm,audio/aac,audio/flac,audio/mp4,application/zip,application/x-tar,application/gzip,application/x-rar-compressed,application/x-7z-compressed,application/x-bzip2

# S3 Configuration (when FILE_STORAGE_BACKEND=s3)
S3_BUCKET=alian-structure-files
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,12 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: typescript-client
path: client/
path: sdks/typescript
retention-days: 30

- name: Upload Python client artifact
uses: actions/upload-artifact@v4
with:
name: python-client
path: sdks/python
retention-days: 30
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,32 @@ The backend supports two authentication methods:

All authentication endpoints use JWT tokens for session management with bcrypt password hashing for traditional auth.

#### Token lifetimes and algorithm

Token lifetimes and the signing algorithm are defined once, in
[`src/config/jwt.config.ts`](src/config/jwt.config.ts), and referenced by both
`AuthService` and `EnhancedAuthService`:

| Token | Lifetime | Constant |
| --- | --- | --- |
| Access token | 15 minutes | `ACCESS_TOKEN_TTL` |
| Refresh token | 7 days | `REFRESH_TOKEN_TTL_MS` |

Tokens are signed with `HS256` and verification is restricted to an explicit
`algorithms: ['HS256']` allowlist; a token presented under any other algorithm is
rejected rather than the verifier choosing for itself.

Clients must treat an expired access token as a normal condition and exchange it
for a new one via the refresh endpoint - a 15-minute access token means an
unattended client will need to refresh roughly every quarter hour.

> **Upgrade note.** `EnhancedAuthService` previously issued access tokens via
> `jwtService.sign(payload)` with no options, so the lifetime was whatever the
> `JwtModule` default happened to be. Sessions issued before this change keep
> whatever lifetime they were minted with until they expire; only newly issued
> tokens are guaranteed to carry a 15-minute `exp`. The refresh token lifetime
> is unchanged.

### Job Control API

Fine-grained control over compute jobs with role-based access control:
Expand Down
19 changes: 14 additions & 5 deletions docs/kubernetes-health-probes.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,20 @@ All endpoints are public (no authentication required) and excluded from rate lim

### Status values

| Value | Meaning |
|---|---|
| `ok` | All components healthy |
| `degraded` | Some non-critical components down (readiness only) |
| `error` | One or more critical components down — returns HTTP 503 |
| Enum (`HealthStatus`) | Value | Meaning | HTTP Status Mapping |
|---|---|---|---|
| `HealthStatus.OK` | `ok` | All components healthy | `200 OK` |
| `HealthStatus.DEGRADED` | `degraded` | Some non-critical components down (readiness only) | `200 OK` |
| `HealthStatus.ERROR` | `error` | One or more critical components down | `503 Service Unavailable` |

### Component status values

| Enum (`ComponentStatus`) | Value | Meaning |
|---|---|---|
| `ComponentStatus.UP` | `up` | Subsystem responding within timeout threshold |
| `ComponentStatus.DOWN` | `down` | Subsystem unreachable or throwing error |
| `ComponentStatus.DEGRADED` | `degraded` | Subsystem responding with high latency or partial capacity |
| `ComponentStatus.UNKNOWN` | `unknown` | Status evaluation skipped or unconfigured |

## Kubernetes probe configuration

Expand Down
8 changes: 8 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
// Unit specs are colocated with the code they cover, so `rootDir` is `src`.
//
// NOTE: because rootDir is `src`, a `*.spec.ts` written anywhere else in the
// repository matches `testRegex` below but is *never executed* - `npm test`
// still passes, which makes the gap invisible. Tests that need the whole app
// (supertest, TypeOrmModule.forRoot, dropSchema) belong in `test/` and must
// use the `.e2e-spec.ts` suffix so that test/jest-e2e.json picks them up.
// src/config/jest-project-coverage.spec.ts enforces both halves of this.
rootDir: 'src',
testRegex: '.*\\.spec\\.ts$',
transform: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Test, TestingModule } from "@nestjs/testing";
import { ConfigService } from "@nestjs/config";
import { Wallet } from "ethers";
import { PayloadSigningService } from "../../src/blockchain/oracle/services/payload-signing.service";
import { HDNodeWallet } from "ethers";
import { PayloadSigningService } from "./payload-signing.service";

describe("PayloadSigningService", () => {
let service: PayloadSigningService;
let testWallet: Wallet;
let testWallet: HDNodeWallet;
let configService: ConfigService;

beforeEach(async () => {
// Create a test wallet
testWallet = Wallet.createRandom();
testWallet = HDNodeWallet.createRandom();

// Mock ConfigService
const mockConfigService = {
Expand Down Expand Up @@ -207,7 +207,7 @@ describe("PayloadSigningService", () => {
payload,
);

const differentWallet = Wallet.createRandom();
const differentWallet = HDNodeWallet.createRandom();

const isValid = service.verifySignature(
signature,
Expand Down
63 changes: 63 additions & 0 deletions src/common/utils/random-id.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as fs from "fs";
import * as path from "path";

/**
* Regression guard for the `Math.random()` -> `crypto.randomUUID()` migration.
*
* The identifiers that were generated with `Math.random()` are not just ugly:
* they are the members of the rate-limiter sliding-window sorted set and the
* ids of persisted, externally visible rate-limit violation records. A
* non-cryptographic PRNG makes those values predictable and collision-prone,
* which is a correctness problem rather than a style one.
*
* This test fails the moment somebody reintroduces `Math.random()` in the
* directories covered by the issue, so the guarantee does not depend on
* reviewer vigilance.
*/

const SRC_ROOT = path.resolve(__dirname, "..", "..");

/** Directories in scope, per the issue's acceptance criteria. */
const GUARDED_DIRECTORIES = [
path.join(SRC_ROOT, "rate-limiting"),
path.join(SRC_ROOT, "observability"),
path.join(SRC_ROOT, "common", "middleware"),
// The issue lists the profiling middleware/service by their pre-move paths
// (src/common/middleware/profiling.middleware.ts and
// src/observability/profiling.service.ts); in the current tree they live in
// src/profiling, so that is the directory that has to stay clean.
path.join(SRC_ROOT, "profiling"),
];

/**
* Strip comments so the explanatory comments that *mention* `Math.random()` do
* not trip the guard. Only executable code is interesting here.
*/
function stripComments(source: string): string {
return source
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/(^|[^:])\/\/.*$/gm, "$1");
}

function collectTypeScriptFiles(dir: string): string[] {
return fs
.readdirSync(dir, { withFileTypes: true })
.flatMap((entry) => {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) return collectTypeScriptFiles(entryPath);
return entry.isFile() && entry.name.endsWith(".ts") ? [entryPath] : [];
});
}

describe("Math.random() is not used to build identifiers", () => {
it.each(GUARDED_DIRECTORIES)(
"has no Math.random() call sites in %s",
(dir) => {
const offenders = collectTypeScriptFiles(dir)
.filter((file) => /Math\s*\.\s*random\s*\(/.test(stripComments(fs.readFileSync(file, "utf8"))))
.map((file) => path.relative(SRC_ROOT, file));

expect(offenders).toEqual([]);
},
);
});
84 changes: 84 additions & 0 deletions src/common/utils/random-id.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
RANDOM_ID_ENTROPY_BITS,
UUID_V4_PATTERN,
generatePrefixedTimestampedId,
generateRandomId,
generateTimestampedId,
} from "./random-id";

describe("random-id", () => {
describe("generateRandomId", () => {
it("returns an RFC 4122 v4 UUID", () => {
expect(generateRandomId()).toMatch(UUID_V4_PATTERN);
});

it("stays unique across 100k rapid invocations", () => {
const COUNT = 100_000;
const seen = new Set<string>();

for (let i = 0; i < COUNT; i++) {
seen.add(generateRandomId());
}

expect(seen.size).toBe(COUNT);
});
});

describe("generateTimestampedId", () => {
it("keeps the <epochMillis>-<uuid> shape used by the rate-limiting API", () => {
const [timestamp, uuid] = generateTimestampedId().split("-", 2);

expect(Number(timestamp)).toBeGreaterThan(0);
expect(uuid).toMatch(UUID_V4_PATTERN);
});

it("stays unique across 100k rapid invocations within a single millisecond", () => {
// The whole point of the CSPRNG suffix: the timestamp prefix alone is
// *not* unique, because every call in a tight loop lands in the same
// millisecond. Two violations recorded in that millisecond used to be
// able to collide.
const COUNT = 100_000;
const seen = new Set<string>();
const prefixes = new Set<string>();

for (let i = 0; i < COUNT; i++) {
const id = generateTimestampedId();
prefixes.add(id.slice(0, id.indexOf("-")));
seen.add(id);
}

expect(seen.size).toBe(COUNT);
// Sanity check that the collisions we are guarding against are real:
// the generator genuinely ran inside a small number of milliseconds.
expect(prefixes.size).toBeLessThan(COUNT);
});
});

describe("generatePrefixedTimestampedId", () => {
it("prefixes the id with the supplied type segment", () => {
expect(generatePrefixedTimestampedId("cpu")).toMatch(
/^cpu-\d{13}-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
expect(generatePrefixedTimestampedId("heap")).toMatch(
/^heap-\d{13}-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
});

it("stays unique across 100k rapid invocations", () => {
const COUNT = 100_000;
const seen = new Set<string>();

for (let i = 0; i < COUNT; i++) {
seen.add(generatePrefixedTimestampedId("cpu"));
}

expect(seen.size).toBe(COUNT);
});
});

it("derives ids from a CSPRNG, not Math.random", () => {
// 122 random bits per v4 UUID. A base-36 `Math.random().toString(36)
// .slice(2, 9)` suffix carried well under 32 bits and collided under load.
expect(RANDOM_ID_ENTROPY_BITS).toBeGreaterThanOrEqual(122);
});
});
68 changes: 68 additions & 0 deletions src/common/utils/random-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { randomUUID } from "crypto";

/**
* Cryptographically secure random identifiers.
*
* These replace ad-hoc `Math.random()` identifiers that were previously used
* for rate-limit violation records, sliding-window bucket members and
* profiling/tracing correlation ids.
*
* `Math.random()` is a non-cryptographic PRNG: it is not seeded from a CSPRNG,
* its output is predictable, and collisions are realistic for the short
* base-36 suffixes that were being generated. That matters because the values
* produced here are:
*
* - written as *members* of the rate-limiter sliding-window sorted set, where a
* collision means one request evicts or double-counts another client's window
* entry (a rate-limit bypass under load), and
* - returned to clients as rate-limit violation `id` values and persisted into
* the audit trail, where a collision makes two distinct violations
* indistinguishable.
*
* `crypto.randomUUID()` (RFC 4122 v4) draws 122 bits from the platform CSPRNG,
* so collisions are not a practical concern.
*/

/** Number of random bits in a v4 UUID, before the version/variant bits. */
export const RANDOM_ID_ENTROPY_BITS = 122;

/**
* Canonical RFC 4122 v4 UUID string (36 chars, hyphenated).
*
* Use for correlation/tracing ids where the value is opaque and only needs to
* be globally unique.
*/
export function generateRandomId(): string {
return randomUUID();
}

/**
* `<epochMillis>-<uuid>` request/record identifier.
*
* The leading epoch-millisecond prefix is retained deliberately: several of
* these ids are externally visible on the rate-limiting API and are used to
* build on-disk profile filenames, where the timestamp keeps the entries
* lexicographically sortable. The suffix is now a CSPRNG UUID instead of a
* `Math.random()` base-36 fragment, which is what actually guarantees
* uniqueness.
*/
export function generateTimestampedId(): string {
return `${Date.now()}-${randomUUID()}`;
}

/**
* `<type>-<epochMillis>-<uuid>` identifier.
*
* Used for profiling artefacts (CPU profiles, heap snapshots) where the `type`
* segment makes the on-disk filename self-describing.
*/
export function generatePrefixedTimestampedId(prefix: string): string {
return `${prefix}-${Date.now()}-${randomUUID()}`;
}

/**
* RFC 4122 v4 UUID shape, used to assert in tests that generated ids keep the
* collision-resistant shape after the `Math.random()` removal.
*/
export const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
Loading