Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/controllers/complianceController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { prisma } from "../config/database";
import { AppError } from "../middleware/errorHandler";
import { logger } from "../config/logger";
import crypto from "crypto";
import { generateId } from '../utils/idGenerator';

/**
* GET /compliance/export
Expand Down Expand Up @@ -88,7 +89,7 @@ export async function deleteAccount(
await tx.guardian.deleteMany({ where: { guardianUserId: userId } });

// 2. Tombstone the User record
const tombstoneSuffix = crypto.randomUUID().substring(0, 8);
const tombstoneSuffix = generateId().substring(0, 8);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid timestamp-prefixed tombstone suffixes

Line 92 takes substring(0, 8) from generateId(). For UUIDv7-style IDs, the prefix is time-derived, so suffixes become predictable and can collide under burst deletes. Use random tail bits instead of the leading timestamp portion.

Suggested fix
-      const tombstoneSuffix = generateId().substring(0, 8);
+      const tombstoneSuffix = generateId().replace(/-/g, "").slice(-8);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tombstoneSuffix = generateId().substring(0, 8);
const tombstoneSuffix = generateId().replace(/-/g, "").slice(-8);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/complianceController.ts` at line 92, The tombstoneSuffix
generation in the complianceController is using the first 8 characters of
generateId() which captures the time-derived prefix of UUIDv7-style IDs, making
the suffix predictable and prone to collisions during burst deletes. Fix this by
changing the substring call in the tombstoneSuffix assignment to use the random
tail bits of the ID instead of the leading timestamp portion, such as using
substring with indices that target the end of the generated ID rather than the
beginning.

await tx.user.update({
where: { id: userId },
data: {
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/kycController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { Response, NextFunction } from "express";
import { z } from "zod";
import crypto from "crypto";
import { generateId } from '../utils/idGenerator';
import { AuthRequest } from "../middleware/auth";
import { prisma } from "../config/database";
import { AppError } from "../middleware/errorHandler";
Expand Down Expand Up @@ -101,7 +102,7 @@ export async function requestUploadUrl(
// Generate a document ID if not provided
const documentId =
body.document_id ??
crypto.randomUUID();
generateId();

const result = await generateUploadUrl(
userId,
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/onrampController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { Response, NextFunction } from "express";
import { z } from "zod";
import crypto from "crypto";
import { generateId } from '../utils/idGenerator';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix Prettier violations on the new import and correlation fallback formatting.

Line 8 and Lines 127-129 are currently failing ESLint/Prettier.

Suggested patch
-import { generateId } from '../utils/idGenerator';
+import { generateId } from "../utils/idGenerator";
@@
-    const correlationId =
-      (req.headers["x-request-id"] as string | undefined) ??
-      generateId();
+    const correlationId =
+      (req.headers["x-request-id"] as string | undefined) ?? generateId();

Also applies to: 127-129

🧰 Tools
🪛 ESLint

[error] 8-8: Replace '../utils/idGenerator' with "../utils/idGenerator"

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/onrampController.ts` at line 8, Fix the Prettier formatting
violations in the onrampController.ts file. On line 8, adjust the import
statement for generateId to comply with Prettier's formatting rules by checking
line length, spacing, and proper formatting. Additionally, on lines 127-129, fix
the correlation fallback formatting to meet ESLint/Prettier standards by
reviewing the multi-line construct formatting, indentation, and spacing. Run
Prettier on the file to automatically format these sections correctly, or
manually adjust the formatting to match your project's Prettier configuration
(including proper line breaks, indentation, and spacing conventions).

Source: Linters/SAST tools

import { prisma } from "../config/database";
import { AuthRequest } from "../middleware/auth";
import { Decimal } from "@prisma/client/runtime/library";
Expand Down Expand Up @@ -125,7 +126,7 @@ export async function registerOnRampSwap(
}
const correlationId =
(req.headers["x-request-id"] as string | undefined) ??
crypto.randomUUID();
generateId();
logFinancialEvent({
event: "onramp.registered",
status: "pending",
Expand Down
3 changes: 2 additions & 1 deletion src/controllers/userController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import crypto from "crypto";
import { generateId } from '../utils/idGenerator';
import { Response, NextFunction } from "express";
import type { Prisma } from "@prisma/client";
import { z } from "zod";
Expand Down Expand Up @@ -565,7 +566,7 @@ export async function deleteMe(
await tx.guardian.deleteMany({ where: { guardianUserId: userId } });

// 2. Tombstone the User record
const tombstoneSuffix = crypto.randomUUID().substring(0, 8);
const tombstoneSuffix = generateId().substring(0, 8);
await tx.user.update({
where: { id: userId },
data: {
Expand Down
5 changes: 3 additions & 2 deletions src/controllers/webhookController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function setFiatWebhookDeprecationHeaders(res: Response): void {
}
import { Request, Response, NextFunction } from "express";
import crypto from "crypto";
import { generateId } from '../utils/idGenerator';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Resolve ESLint/Prettier formatting errors in the touched webhook paths.

Line 17 and Lines 221-223/302-304 are flagged by Prettier in the current diff.

Suggested patch
-import { generateId } from '../utils/idGenerator';
+import { generateId } from "../utils/idGenerator";
@@
-    const paystackCorrelationId =
-      (req.headers["x-request-id"] as string | undefined) ??
-      generateId();
+    const paystackCorrelationId =
+      (req.headers["x-request-id"] as string | undefined) ?? generateId();
@@
-    const flwCorrelationId =
-      (req.headers["x-request-id"] as string | undefined) ??
-      generateId();
+    const flwCorrelationId =
+      (req.headers["x-request-id"] as string | undefined) ?? generateId();

Also applies to: 221-223, 302-304

🧰 Tools
🪛 ESLint

[error] 17-17: Replace '../utils/idGenerator' with "../utils/idGenerator"

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/webhookController.ts` at line 17, The import statement for
generateId on line 17 and the code sections at lines 221-223 and 302-304 in
webhookController.ts have Prettier formatting violations. Run Prettier on the
entire file to automatically resolve all formatting issues across these
sections. This will ensure consistent formatting throughout the file in
compliance with the project's Prettier configuration.

Source: Linters/SAST tools

import { config } from "../config/env";
import { logger, logFinancialEvent } from "../config/logger";
import { prisma } from "../config/database";
Expand Down Expand Up @@ -219,7 +220,7 @@ export async function handlePaystackWebhook(
paystackStatusMap[data.status ?? ""] ?? "pending";
const paystackCorrelationId =
(req.headers["x-request-id"] as string | undefined) ??
crypto.randomUUID();
generateId();

logFinancialEvent({
event: "webhook.received",
Expand Down Expand Up @@ -300,7 +301,7 @@ export async function handleFlutterwaveWebhook(
flwStatusMap[data.status ?? ""] ?? "pending";
const flwCorrelationId =
(req.headers["x-request-id"] as string | undefined) ??
crypto.randomUUID();
generateId();

logFinancialEvent({
event: "webhook.received",
Expand Down
4 changes: 2 additions & 2 deletions src/jobs/withdrawalProcessingJob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Consumes WITHDRAWAL_PROCESSING queue: after BurnEvent, validate withdrawal and recipient,
* disburse via fintech, update transaction status, optionally publish user notification.
*/
import { randomUUID } from "crypto";
import { generateId } from '../utils/idGenerator';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix import quote style to satisfy Prettier.

Line 5 is currently flagged by ESLint/Prettier.

Suggested patch
-import { generateId } from '../utils/idGenerator';
+import { generateId } from "../utils/idGenerator";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { generateId } from '../utils/idGenerator';
import { generateId } from "../utils/idGenerator";
🧰 Tools
🪛 ESLint

[error] 5-5: Replace '../utils/idGenerator' with "../utils/idGenerator"

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jobs/withdrawalProcessingJob.ts` at line 5, The import statement for
generateId from '../utils/idGenerator' on line 5 uses single quotes which does
not comply with Prettier's quote style configuration. Change the single quotes
around the import path '../utils/idGenerator' to double quotes to satisfy
Prettier's formatting rules.

Source: Linters/SAST tools

import type { ConsumeMessage } from "amqplib";
import {
connectRabbitMQ,
Expand All @@ -28,7 +28,7 @@ export async function startWithdrawalProcessingConsumer(): Promise<void> {
queue,
async (msg: ConsumeMessage | null) => {
if (!msg) return;
const correlationId = randomUUID();
const correlationId = generateId();
try {
const body = JSON.parse(msg.content.toString()) as WithdrawalPayload;
const { transactionId, txHash } = body;
Expand Down
6 changes: 3 additions & 3 deletions src/jobs/xlmToAcbuJob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { logger, logFinancialEvent } from "../config/logger";
import { prisma } from "../config/database";
import { mintFromUsdcInternal } from "../controllers/mintController";
import { fetchXlmRateUsd } from "../services/oracle/cryptoClient";
import { randomUUID } from "crypto";
import { generateId } from '../utils/idGenerator';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Normalize import quotes to pass Prettier.

Line 12 is reported as a Prettier violation.

Suggested patch
-import { generateId } from '../utils/idGenerator';
+import { generateId } from "../utils/idGenerator";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { generateId } from '../utils/idGenerator';
import { generateId } from "../utils/idGenerator";
🧰 Tools
🪛 ESLint

[error] 12-12: Replace '../utils/idGenerator' with "../utils/idGenerator"

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jobs/xlmToAcbuJob.ts` at line 12, The import statement for generateId
from '../utils/idGenerator' on line 12 violates Prettier formatting rules due to
incorrect quote style. Normalize the quotes around the module path in this
import statement to match your project's Prettier configuration, likely
requiring double quotes instead of single quotes around the module path string.

Source: Linters/SAST tools


const QUEUE = QUEUES.XLM_TO_ACBU;
const MAX_RETRIES = 5;
Expand All @@ -34,7 +34,7 @@ export async function startXlmToAcbuConsumer(): Promise<void> {
const retries = typeof headers["x-retries"] === "number" ? headers["x-retries"] : 0;
try {
const body = JSON.parse(msg.content.toString()) as XlmToAcbuPayload;
const correlationId = randomUUID();
const correlationId = generateId();
await processXlmToAcbu(body, correlationId);
ch.ack(msg);
} catch (e) {
Expand All @@ -61,7 +61,7 @@ export async function startXlmToAcbuConsumer(): Promise<void> {
*/
export async function processXlmToAcbu(
payload: XlmToAcbuPayload,
correlationId: string = randomUUID(),
correlationId: string = generateId(),
): Promise<void> {
const { onRampSwapId, userId, stellarAddress, xlmAmount, usdcEquivalent } =
payload;
Expand Down
10 changes: 5 additions & 5 deletions src/services/auth/authService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import bcrypt from "bcryptjs";
import { totp } from "otplib";
import { randomUUID } from "crypto";
import { generateId } from '../../utils/idGenerator';
import { config } from "../../config/env";
import { prisma } from "../../config/database";
import { generateApiKey } from "../../middleware/auth";
Expand Down Expand Up @@ -889,8 +889,8 @@ export interface RevokeRefreshTokenParams {
}

function generateSecureRefreshToken(): string {
const bytes = Buffer.from(randomUUID()).toString('base64');
return bytes + Buffer.from(randomUUID()).toString('base64');
const bytes = Buffer.from(generateId()).toString('base64');
return bytes + Buffer.from(generateId()).toString('base64');
Comment on lines +892 to +893

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use CSPRNG bytes for refresh-token secrets

Line 892 and Line 893 now build bearer refresh tokens from generateId(), which embeds timestamp/counter structure. That makes tokens more predictable than necessary and leaks issuance timing. Keep generateId() for tokenFamilyId, but generate refresh-token secrets from cryptographically random bytes.

Suggested fix
 function generateSecureRefreshToken(): string {
-  const bytes = Buffer.from(generateId()).toString('base64');
-  return bytes + Buffer.from(generateId()).toString('base64');
+  return randomBytes(48).toString("base64url");
 }
🧰 Tools
🪛 ESLint

[error] 892-892: Replace 'base64' with "base64"

(prettier/prettier)


[error] 893-893: Replace 'base64' with "base64"

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/auth/authService.ts` around lines 892 - 893, The refresh-token
secret generation in the code around lines 892-893 is using generateId() which
produces predictable tokens with embedded timestamp/counter structure. Replace
the Buffer.from(generateId()).toString('base64') calls used for the
refresh-token secret with cryptographically random bytes generated from a CSPRNG
(such as crypto.randomBytes()). Keep using generateId() only for the
tokenFamilyId if present, but ensure the actual refresh-token secret portion
uses properly random cryptographic bytes instead.

}

async function hashRefreshToken(token: string): Promise<string> {
Expand All @@ -907,7 +907,7 @@ export async function issueRefreshToken(
const { userId } = params;
const token = generateSecureRefreshToken();
const tokenHash = await hashRefreshToken(token);
const tokenFamilyId = randomUUID();
const tokenFamilyId = generateId();
const expiresAt = new Date(
Date.now() + REFRESH_TOKEN_EXPIRY_DAYS * 24 * 60 * 60 * 1000,
);
Expand Down Expand Up @@ -995,7 +995,7 @@ export async function refreshAccessToken(
// Issue a new refresh token in a NEW family
const newToken = generateSecureRefreshToken();
const newTokenHash = await hashRefreshToken(newToken);
const newTokenFamilyId = randomUUID();
const newTokenFamilyId = generateId();
const newExpiresAt = new Date(
Date.now() + REFRESH_TOKEN_EXPIRY_DAYS * 24 * 60 * 60 * 1000,
);
Expand Down
7 changes: 4 additions & 3 deletions src/services/bills/billsService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Decimal } from "@prisma/client/runtime/library";
import { prisma } from "../../config/database";
import { generateId } from '../../utils/idGenerator';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix import quote style flagged by ESLint/Prettier.

Line 3 is failing the configured Prettier rule.

Suggested patch
-import { generateId } from '../../utils/idGenerator';
+import { generateId } from "../../utils/idGenerator";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { generateId } from '../../utils/idGenerator';
import { generateId } from "../../utils/idGenerator";
🧰 Tools
🪛 ESLint

[error] 3-3: Replace '../../utils/idGenerator' with "../../utils/idGenerator"

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/bills/billsService.ts` at line 3, The import statement for
generateId from the idGenerator utility is using single quotes around the module
path, which violates the configured Prettier quote style rule. Change the single
quotes to double quotes in the import statement where generateId is imported
from '../../utils/idGenerator' to match the project's quote style configuration.

Source: Linters/SAST tools

import { AppError } from "../../middleware/errorHandler";
import { logger, logFinancialEvent } from "../../config/logger";
import { logAudit } from "../audit";
Expand Down Expand Up @@ -204,7 +205,7 @@ export async function payBill(
performedBy: request.userId ?? undefined,
});

const correlationId = crypto.randomUUID();
const correlationId = generateId();

logFinancialEvent({
event: "bill.initiated",
Expand Down Expand Up @@ -433,7 +434,7 @@ export async function reconcileBillsWebhook(event: BillsWebhookEvent): Promise<{
userId: transaction.userId ?? transaction.id,
accountId: transaction.userId ?? transaction.id,
idempotencyKey: transaction.id,
correlationId: crypto.randomUUID(),
correlationId: generateId(),
amount: Math.round((transaction.localAmount?.toNumber() ?? event.amount) * 100),
currency: transaction.localCurrency ?? event.currency,
provider: event.provider,
Expand Down Expand Up @@ -497,7 +498,7 @@ export async function refundBillPayment(
userId: transaction.userId ?? transaction.id,
accountId: transaction.userId ?? transaction.id,
idempotencyKey: transaction.id,
correlationId: crypto.randomUUID(),
correlationId: generateId(),
amount: Math.round(localAmount * 100),
currency,
provider: refundResponse.provider,
Expand Down
3 changes: 2 additions & 1 deletion src/services/bills/simulatedBillsPartner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
PartnerBillRefundRequest,
PartnerBillRefundResponse,
} from "./types";
import { generateId } from '../../utils/idGenerator';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Adjust import quotes to match Prettier config.

Line 10 is flagged by ESLint/Prettier.

Suggested patch
-import { generateId } from '../../utils/idGenerator';
+import { generateId } from "../../utils/idGenerator";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { generateId } from '../../utils/idGenerator';
import { generateId } from "../../utils/idGenerator";
🧰 Tools
🪛 ESLint

[error] 10-10: Replace '../../utils/idGenerator' with "../../utils/idGenerator"

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/bills/simulatedBillsPartner.ts` at line 10, The import statement
for generateId from utils/idGenerator on line 10 uses single quotes but does not
match the project's Prettier configuration for quote style. Change the quotes
around the import path '../../utils/idGenerator' to match the Prettier config
(typically double quotes are the default). Update the import statement to use
the correct quote style consistently with the rest of the codebase.

Source: Linters/SAST tools


const SIMULATED_BILLERS: BillsCatalogBiller[] = [
{
Expand Down Expand Up @@ -107,7 +108,7 @@ export class SimulatedBillsPartner implements BillsPartnerAdapter {
async payBill(
request: PartnerBillPaymentRequest,
): Promise<PartnerBillPaymentResponse> {
const providerReference = `bill_${crypto.randomUUID()}`;
const providerReference = `bill_${generateId()}`;
return {
provider: this.providerId,
providerReference,
Expand Down
5 changes: 3 additions & 2 deletions src/services/salary/salaryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { logger, logFinancialEvent } from "../../config/logger";
import { CreateSalaryBatchParams, CreateSalaryBatchResult } from "./types";
import { AppError } from "../../middleware/errorHandler";
import crypto from "crypto";
import { generateId } from '../../utils/idGenerator';

/**
* Creates a new salary batch with items. Supports idempotency via idempotencyKey.
Expand Down Expand Up @@ -74,7 +75,7 @@ export async function createSalaryBatch(
organizationId,
});

const salaryCorrelationId = idempotencyKey ?? crypto.randomUUID();
const salaryCorrelationId = idempotencyKey ?? generateId();
logFinancialEvent({
event: "salary.batch.initiated",
status: "pending",
Expand Down Expand Up @@ -205,7 +206,7 @@ export async function processSalaryBatch(batchId: string): Promise<void> {
idempotencyKey: batch.idempotencyKey ?? batchId,
amount: Math.round(batch.totalAmount.toNumber() * 100),
currency: batch.currency,
correlationId: crypto.randomUUID(),
correlationId: generateId(),
});

logger.info("Salary batch processing finished", {
Expand Down
3 changes: 2 additions & 1 deletion src/services/transfer/transferService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { stellarClient } from "../stellar/client";
import { getBaseFee } from "../stellar/feeManager";
import { resolveRecipientToStellarAddress } from "../recipient/recipientResolver";
import crypto from "crypto";
import { generateId } from '../../utils/idGenerator';

import { logger, logFinancialEvent } from "../../config/logger";
import type {
Expand Down Expand Up @@ -154,7 +155,7 @@ export async function createTransfer(
throw createError;
}

const correlationId = options?.correlationId ?? crypto.randomUUID();
const correlationId = options?.correlationId ?? generateId();
// Avoid float arithmetic: parse integer and fractional parts separately to
// prevent precision loss when amount has up to 7 decimal places.
const [wholePart, fracPart = ""] = amount.split(".");
Expand Down
38 changes: 38 additions & 0 deletions src/utils/idGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { randomBytes } from 'crypto';

/**
* Generates a UUIDv7 (timestamp-ordered) string.
* Timestamp-ordered UUIDs avoid B-tree index fragmentation in PostgreSQL
* compared to random UUIDv4, improving insert performance and index locality.
* A monotonic counter ensures strict ordering within the same millisecond.
*/

let lastMs = -1n;
let seq = 0;

export function generateId(): string {
let ms = BigInt(Date.now());
if (ms === lastMs) {
seq = (seq + 1) & 0xfff;
if (seq === 0) ms = ++lastMs; // counter overflow: advance clock
} else {
seq = 0;
lastMs = ms;
}
Comment on lines +15 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Monotonic ordering regresses when Date.now() moves backward.

The state machine only checks ms === lastMs. If ms < lastMs (NTP/time skew, or immediately after the overflow branch advanced lastMs), the else path resets lastMs backward and breaks monotonic ordering guarantees.

Suggested fix
 export function generateId(): string {
   let ms = BigInt(Date.now());
+  if (ms < lastMs) ms = lastMs;
+
   if (ms === lastMs) {
     seq = (seq + 1) & 0xfff;
-    if (seq === 0) ms = ++lastMs; // counter overflow: advance clock
+    if (seq === 0) {
+      ms = lastMs + 1n; // counter overflow: advance logical clock
+    }
   } else {
     seq = 0;
-    lastMs = ms;
   }
+  lastMs = ms;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/idGenerator.ts` around lines 15 - 21, The monotonic ordering logic
does not handle the case where the current timestamp moves backward (ms <
lastMs), which can occur due to time skew or NTP adjustments. Currently, the
else branch unconditionally resets lastMs to the new (earlier) value, breaking
monotonicity. Add a condition to check if ms is less than lastMs in addition to
checking if ms equals lastMs. When the clock moves backward, either wait for the
lastMs timestamp to elapse or use lastMs as the basis for the ID and increment
the sequence counter to ensure uniqueness, rather than resetting the sequence.
This ensures the ID generator maintains strictly monotonic ordering regardless
of system clock adjustments.


const rand = randomBytes(8);
const timeLow = Number(ms & BigInt(0xffffffff));
const timeMid = Number((ms >> BigInt(32)) & BigInt(0xffff));
Comment on lines +24 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Timestamp fields are swapped, so this is not UUIDv7-ordered.

Line 24/25 currently maps ms as low-32 then high-16. UUIDv7 requires the first 48 bits to be the millisecond timestamp in big-endian order, so the first 32 bits must be ms >> 16 and the next 16 bits ms & 0xffff. As written, lexical ordering will wrap every ~49.7 days and break the intended index-locality behavior.

Suggested fix
-  const timeLow = Number(ms & BigInt(0xffffffff));
-  const timeMid = Number((ms >> BigInt(32)) & BigInt(0xffff));
+  const timeLow = Number((ms >> BigInt(16)) & BigInt(0xffffffff));
+  const timeMid = Number(ms & BigInt(0xffff));

Also applies to: 32-33

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/idGenerator.ts` around lines 24 - 25, The timestamp fields in the
UUIDv7 generation are incorrectly ordered, breaking lexical ordering. In the
timeLow and timeMid variable assignments (lines 24-25 and also at lines 32-33),
swap the bit extraction logic so that timeLow receives the upper 32 bits of the
millisecond timestamp (ms right-shifted by 16) and timeMid receives the lower 16
bits (ms masked with 0xffff). This ensures the first 48 bits of the UUID
represent the millisecond timestamp in proper big-endian order, which is
required for UUIDv7 index-locality behavior.

const ver = 0x7000 | seq; // version 7 | 12-bit seq
const varRand = 0x80 | (rand[0] & 0x3f); // variant 10xx | random

const hex = (n: number, pad: number) => n.toString(16).padStart(pad, '0');

return [
hex(timeLow, 8),
hex(timeMid, 4),
hex(ver, 4),
hex(varRand, 2) + hex(rand[1], 2),
rand.slice(2).reduce((s, b) => s + hex(b, 2), ''),
].join('-');
}