Update pnpm-lock.yaml to add '@next/bundle-analyzer' and 'webpack-bundle-analyzer' dependencies. Modify nest-cli.json to prevent deletion of output directory during build. Enhance package.json scripts for development and clean commands. Refactor distributed-transaction.service.ts and transaction.service.ts for improved error handling and logging consistency. Update queue-health.controller.ts and csrf.controller.ts for better API documentation. Clean up whitespace and formatting across various files for improved readability.

This commit is contained in:
barsa 2025-09-26 16:30:00 +09:00
parent 4b877fb3e0
commit ac61dd1e17
36 changed files with 6394 additions and 1117 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,85 @@
[{"id":1,"intrinsicName":"any","recursionId":0,"flags":["Any"]},
{"id":2,"intrinsicName":"any","recursionId":1,"flags":["Any"]},
{"id":3,"intrinsicName":"any","recursionId":2,"flags":["Any"]},
{"id":4,"intrinsicName":"any","recursionId":3,"flags":["Any"]},
{"id":5,"intrinsicName":"error","recursionId":4,"flags":["Any"]},
{"id":6,"intrinsicName":"unresolved","recursionId":5,"flags":["Any"]},
{"id":7,"intrinsicName":"any","recursionId":6,"flags":["Any"]},
{"id":8,"intrinsicName":"intrinsic","recursionId":7,"flags":["Any"]},
{"id":9,"intrinsicName":"unknown","recursionId":8,"flags":["Unknown"]},
{"id":10,"intrinsicName":"undefined","recursionId":9,"flags":["Undefined"]},
{"id":11,"intrinsicName":"undefined","recursionId":10,"flags":["Undefined"]},
{"id":12,"intrinsicName":"undefined","recursionId":11,"flags":["Undefined"]},
{"id":13,"intrinsicName":"null","recursionId":12,"flags":["Null"]},
{"id":14,"intrinsicName":"string","recursionId":13,"flags":["String"]},
{"id":15,"intrinsicName":"number","recursionId":14,"flags":["Number"]},
{"id":16,"intrinsicName":"bigint","recursionId":15,"flags":["BigInt"]},
{"id":17,"intrinsicName":"false","recursionId":16,"flags":["BooleanLiteral"],"display":"false"},
{"id":18,"intrinsicName":"false","recursionId":17,"flags":["BooleanLiteral"],"display":"false"},
{"id":19,"intrinsicName":"true","recursionId":18,"flags":["BooleanLiteral"],"display":"true"},
{"id":20,"intrinsicName":"true","recursionId":19,"flags":["BooleanLiteral"],"display":"true"},
{"id":21,"intrinsicName":"boolean","recursionId":20,"unionTypes":[18,20],"flags":["Boolean","BooleanLike","PossiblyFalsy","Union"]},
{"id":22,"intrinsicName":"symbol","recursionId":21,"flags":["ESSymbol"]},
{"id":23,"intrinsicName":"void","recursionId":22,"flags":["Void"]},
{"id":24,"intrinsicName":"never","recursionId":23,"flags":["Never"]},
{"id":25,"intrinsicName":"never","recursionId":24,"flags":["Never"]},
{"id":26,"intrinsicName":"never","recursionId":25,"flags":["Never"]},
{"id":27,"intrinsicName":"never","recursionId":26,"flags":["Never"]},
{"id":28,"intrinsicName":"object","recursionId":27,"flags":["NonPrimitive"]},
{"id":29,"recursionId":28,"unionTypes":[14,15],"flags":["Union"]},
{"id":30,"recursionId":29,"unionTypes":[14,15,22],"flags":["Union"]},
{"id":31,"recursionId":30,"unionTypes":[15,16],"flags":["Union"]},
{"id":32,"recursionId":31,"unionTypes":[10,13,14,15,16,18,20],"flags":["Union"]},
{"id":33,"recursionId":32,"flags":["TemplateLiteral"]},
{"id":34,"intrinsicName":"never","recursionId":33,"flags":["Never"]},
{"id":35,"recursionId":34,"flags":["Object"],"display":"{}"},
{"id":36,"recursionId":35,"flags":["Object"],"display":"{}"},
{"id":37,"recursionId":36,"flags":["Object"],"display":"{}"},
{"id":38,"symbolName":"__type","recursionId":37,"flags":["Object"],"display":"{}"},
{"id":39,"recursionId":38,"flags":["Object"],"display":"{}"},
{"id":40,"recursionId":39,"unionTypes":[10,13,39],"flags":["Union"]},
{"id":41,"recursionId":40,"flags":["Object"],"display":"{}"},
{"id":42,"recursionId":41,"flags":["Object"],"display":"{}"},
{"id":43,"recursionId":42,"flags":["Object"],"display":"{}"},
{"id":44,"recursionId":43,"flags":["Object"],"display":"{}"},
{"id":45,"recursionId":44,"flags":["Object"],"display":"{}"},
{"id":46,"flags":["TypeParameter","IncludesMissingType"]},
{"id":47,"flags":["TypeParameter","IncludesMissingType"]},
{"id":48,"flags":["TypeParameter","IncludesMissingType"]},
{"id":49,"flags":["TypeParameter","IncludesMissingType"]},
{"id":50,"flags":["TypeParameter","IncludesMissingType"]},
{"id":51,"recursionId":45,"flags":["StringLiteral"],"display":"\"\""},
{"id":52,"recursionId":46,"flags":["NumberLiteral"],"display":"0"},
{"id":53,"recursionId":47,"flags":["BigIntLiteral"],"display":"0n"},
{"id":54,"recursionId":48,"flags":["StringLiteral"],"display":"\"string\""},
{"id":55,"recursionId":49,"flags":["StringLiteral"],"display":"\"number\""},
{"id":56,"recursionId":50,"flags":["StringLiteral"],"display":"\"bigint\""},
{"id":57,"recursionId":51,"flags":["StringLiteral"],"display":"\"boolean\""},
{"id":58,"recursionId":52,"flags":["StringLiteral"],"display":"\"symbol\""},
{"id":59,"recursionId":53,"flags":["StringLiteral"],"display":"\"undefined\""},
{"id":60,"recursionId":54,"flags":["StringLiteral"],"display":"\"object\""},
{"id":61,"recursionId":55,"flags":["StringLiteral"],"display":"\"function\""},
{"id":62,"recursionId":56,"unionTypes":[54,55,56,57,58,59,60,61],"flags":["Union"]},
{"id":63,"symbolName":"IArguments","recursionId":57,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":402,"character":2},"end":{"line":408,"character":2}},"flags":["Object"]},
{"id":64,"symbolName":"globalThis","recursionId":58,"flags":["Object"],"display":"typeof globalThis"},
{"id":65,"symbolName":"Array","recursionId":59,"instantiatedType":65,"typeArguments":[66],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1323,"character":2},"end":{"line":1511,"character":2}},"flags":["Object"]},
{"id":66,"symbolName":"T","recursionId":60,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1325,"character":17},"end":{"line":1325,"character":18}},"flags":["TypeParameter","IncludesMissingType"]},
{"id":67,"symbolName":"Array","recursionId":59,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1323,"character":2},"end":{"line":1511,"character":2}},"flags":["TypeParameter","IncludesMissingType"]},
{"id":68,"symbolName":"Object","recursionId":61,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":121,"character":2},"end":{"line":153,"character":2}},"flags":["Object"]},
{"id":69,"symbolName":"Function","recursionId":62,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":270,"character":39},"end":{"line":307,"character":2}},"flags":["Object"]},
{"id":70,"symbolName":"CallableFunction","recursionId":63,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":329,"character":136},"end":{"line":366,"character":2}},"flags":["Object"]},
{"id":71,"symbolName":"NewableFunction","recursionId":64,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":366,"character":2},"end":{"line":402,"character":2}},"flags":["Object"]},
{"id":72,"symbolName":"String","recursionId":65,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":408,"character":2},"end":{"line":532,"character":2}},"flags":["Object"]},
{"id":73,"symbolName":"Number","recursionId":66,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":557,"character":41},"end":{"line":586,"character":2}},"flags":["Object"]},
{"id":74,"symbolName":"Boolean","recursionId":67,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":544,"character":39},"end":{"line":549,"character":2}},"flags":["Object"]},
{"id":75,"symbolName":"RegExp","recursionId":68,"instantiatedType":75,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":991,"character":2},"end":{"line":1023,"character":2}},"flags":["Object"]},
{"id":76,"symbolName":"RegExp","recursionId":68,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":991,"character":2},"end":{"line":1023,"character":2}},"flags":["TypeParameter","IncludesMissingType"]},
{"id":77,"symbolName":"Array","recursionId":59,"instantiatedType":65,"typeArguments":[1],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1323,"character":2},"end":{"line":1511,"character":2}},"flags":["Object"]},
{"id":78,"symbolName":"Array","recursionId":59,"instantiatedType":65,"typeArguments":[2],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1323,"character":2},"end":{"line":1511,"character":2}},"flags":["Object"]},
{"id":79,"symbolName":"ReadonlyArray","recursionId":69,"instantiatedType":79,"typeArguments":[80],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1185,"character":24},"end":{"line":1316,"character":2}},"flags":["Object"]},
{"id":80,"symbolName":"T","recursionId":70,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1191,"character":25},"end":{"line":1191,"character":26}},"flags":["TypeParameter","IncludesMissingType"]},
{"id":81,"symbolName":"ReadonlyArray","recursionId":69,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1185,"character":24},"end":{"line":1316,"character":2}},"flags":["TypeParameter","IncludesMissingType"]},
{"id":82,"symbolName":"ReadonlyArray","recursionId":69,"instantiatedType":79,"typeArguments":[1],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1185,"character":24},"end":{"line":1316,"character":2}},"flags":["Object"]},
{"id":83,"symbolName":"ThisType","recursionId":71,"instantiatedType":83,"typeArguments":[84],"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1680,"character":29},"end":{"line":1685,"character":25}},"flags":["Object"]},
{"id":84,"symbolName":"T","recursionId":72,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1685,"character":20},"end":{"line":1685,"character":21}},"flags":["TypeParameter","IncludesMissingType"]},
{"id":85,"symbolName":"ThisType","recursionId":71,"firstDeclaration":{"path":"/home/barsa/projects/customer_portal/customer-portal/node_modules/.pnpm/typescript@5.9.2/node_modules/typescript/lib/lib.es5.d.ts","start":{"line":1680,"character":29},"end":{"line":1685,"character":25}},"flags":["TypeParameter","IncludesMissingType"]}]

View File

@ -4,7 +4,7 @@
"sourceRoot": "src",
"compilerOptions": {
"tsConfigPath": "tsconfig.build.json",
"deleteOutDir": true,
"deleteOutDir": false,
"watchAssets": true,
"assets": ["**/*.prisma"]
}

View File

@ -9,7 +9,8 @@
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev": "nest start --watch --preserveWatchOutput",
"predev": "tsc -b --force tsconfig.build.json",
"dev": "nest start -p tsconfig.build.json --watch --preserveWatchOutput",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint .",
@ -21,7 +22,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json",
"type-check": "tsc --project tsconfig.json --noEmit",
"type-check:watch": "tsc --project tsconfig.json --noEmit --watch",
"clean": "rm -rf dist",
"clean": "rm -rf dist tsconfig.build.tsbuildinfo",
"db:migrate": "prisma migrate dev",
"db:generate": "prisma generate",
"db:studio": "prisma studio",

View File

@ -43,7 +43,7 @@ export class DistributedTransactionService {
/**
* Execute a distributed transaction with multiple steps across different systems
*
*
* @example
* ```typescript
* const result = await this.distributedTransactionService.executeDistributedTransaction([
@ -91,16 +91,16 @@ export class DistributedTransactionService {
description,
timeout = 120000, // 2 minutes default for distributed operations
maxRetries = 1, // Less retries for distributed operations
continueOnNonCriticalFailure = false
continueOnNonCriticalFailure = false,
} = options;
const transactionId = this.generateTransactionId();
const startTime = Date.now();
this.logger.log(`Starting distributed transaction [${transactionId}]`, {
description,
stepsCount: steps.length,
timeout
timeout,
});
const stepResults: Record<string, any> = {};
@ -113,29 +113,28 @@ export class DistributedTransactionService {
for (const step of steps) {
this.logger.debug(`Executing step: ${step.id} [${transactionId}]`, {
description: step.description,
critical: step.critical
critical: step.critical,
});
try {
const stepStartTime = Date.now();
const result = await this.executeStepWithTimeout(step, timeout);
const stepDuration = Date.now() - stepStartTime;
stepResults[step.id] = result;
executedSteps.push(step.id);
this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, {
duration: stepDuration
});
this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, {
duration: stepDuration,
});
} catch (stepError) {
lastError = stepError as Error;
failedSteps.push(step.id);
this.logger.error(`Step failed: ${step.id} [${transactionId}]`, {
error: getErrorMessage(stepError),
critical: step.critical,
retryable: step.retryable
retryable: step.retryable,
});
// If it's a critical step, stop the entire transaction
@ -149,17 +148,19 @@ export class DistributedTransactionService {
}
// Otherwise, log and continue
this.logger.warn(`Continuing despite non-critical step failure: ${step.id} [${transactionId}]`);
this.logger.warn(
`Continuing despite non-critical step failure: ${step.id} [${transactionId}]`
);
}
}
const duration = Date.now() - startTime;
this.logger.log(`Distributed transaction completed successfully [${transactionId}]`, {
description,
duration,
stepsExecuted: executedSteps.length,
failedSteps: failedSteps.length
failedSteps: failedSteps.length,
});
return {
@ -169,25 +170,24 @@ export class DistributedTransactionService {
stepsExecuted: executedSteps.length,
stepsRolledBack: 0,
stepResults,
failedSteps
failedSteps,
};
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error(`Distributed transaction failed [${transactionId}]`, {
description,
error: getErrorMessage(error),
duration,
stepsExecuted: executedSteps.length,
failedSteps: failedSteps.length
failedSteps: failedSteps.length,
});
// Execute rollbacks for completed steps
const rollbacksExecuted = await this.executeRollbacks(
steps,
executedSteps,
stepResults,
steps,
executedSteps,
stepResults,
transactionId
);
@ -198,7 +198,7 @@ export class DistributedTransactionService {
stepsExecuted: executedSteps.length,
stepsRolledBack: rollbacksExecuted,
stepResults,
failedSteps
failedSteps,
};
}
}
@ -209,7 +209,7 @@ export class DistributedTransactionService {
async executeHybridTransaction<T>(
databaseOperation: (tx: any, context: TransactionContext) => Promise<T>,
externalSteps: DistributedStep[],
options: DistributedTransactionOptions & {
options: DistributedTransactionOptions & {
databaseFirst?: boolean;
rollbackDatabaseOnExternalFailure?: boolean;
}
@ -222,11 +222,11 @@ export class DistributedTransactionService {
const transactionId = this.generateTransactionId();
const startTime = Date.now();
this.logger.log(`Starting hybrid transaction [${transactionId}]`, {
description: options.description,
databaseFirst,
externalStepsCount: externalSteps.length
externalStepsCount: externalSteps.length,
});
try {
@ -240,12 +240,12 @@ export class DistributedTransactionService {
databaseOperation,
{
description: `${options.description} - Database Operations`,
timeout: options.timeout
timeout: options.timeout,
}
);
if (!dbTransactionResult.success) {
throw new Error(dbTransactionResult.error || 'Database transaction failed');
throw new Error(dbTransactionResult.error || "Database transaction failed");
}
databaseResult = dbTransactionResult.data!;
@ -254,27 +254,29 @@ export class DistributedTransactionService {
this.logger.debug(`Executing external operations [${transactionId}]`);
externalResult = await this.executeDistributedTransaction(externalSteps, {
...distributedOptions,
description: distributedOptions.description || 'External operations'
description: distributedOptions.description || "External operations",
});
if (!externalResult.success && rollbackDatabaseOnExternalFailure) {
// Note: Database transaction already committed, so we can't rollback automatically
// This is a limitation of this approach - consider using saga pattern for true rollback
this.logger.error(`External operations failed but database already committed [${transactionId}]`, {
externalError: externalResult.error
});
this.logger.error(
`External operations failed but database already committed [${transactionId}]`,
{
externalError: externalResult.error,
}
);
}
} else {
// Execute external operations first
this.logger.debug(`Executing external operations [${transactionId}]`);
externalResult = await this.executeDistributedTransaction(externalSteps, {
...distributedOptions,
description: distributedOptions.description || 'External operations'
description: distributedOptions.description || "External operations",
});
if (!externalResult.success) {
throw new Error(externalResult.error || 'External operations failed');
throw new Error(externalResult.error || "External operations failed");
}
// Execute database operations
@ -283,7 +285,7 @@ export class DistributedTransactionService {
databaseOperation,
{
description: `${options.description} - Database Operations`,
timeout: options.timeout
timeout: options.timeout,
}
);
@ -295,17 +297,17 @@ export class DistributedTransactionService {
externalResult.stepResults,
transactionId
);
throw new Error(dbTransactionResult.error || 'Database transaction failed');
throw new Error(dbTransactionResult.error || "Database transaction failed");
}
databaseResult = dbTransactionResult.data!;
}
const duration = Date.now() - startTime;
this.logger.log(`Hybrid transaction completed successfully [${transactionId}]`, {
description: options.description,
duration
duration,
});
return {
@ -315,16 +317,15 @@ export class DistributedTransactionService {
stepsExecuted: externalResult?.stepsExecuted || 0,
stepsRolledBack: 0,
stepResults: externalResult?.stepResults || {},
failedSteps: externalResult?.failedSteps || []
failedSteps: externalResult?.failedSteps || [],
};
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error(`Hybrid transaction failed [${transactionId}]`, {
description: options.description,
error: getErrorMessage(error),
duration
duration,
});
return {
@ -334,7 +335,7 @@ export class DistributedTransactionService {
stepsExecuted: 0,
stepsRolledBack: 0,
stepResults: {},
failedSteps: []
failedSteps: [],
};
}
}
@ -346,7 +347,7 @@ export class DistributedTransactionService {
setTimeout(() => {
reject(new Error(`Step ${step.id} timed out after ${timeout}ms`));
}, timeout);
})
}),
]);
}
@ -357,14 +358,14 @@ export class DistributedTransactionService {
transactionId: string
): Promise<number> {
this.logger.warn(`Executing rollbacks for ${executedSteps.length} steps [${transactionId}]`);
let rollbacksExecuted = 0;
// Execute rollbacks in reverse order (LIFO)
for (let i = executedSteps.length - 1; i >= 0; i--) {
const stepId = executedSteps[i];
const step = steps.find(s => s.id === stepId);
if (step?.rollback) {
try {
this.logger.debug(`Executing rollback for step: ${stepId} [${transactionId}]`);
@ -373,7 +374,7 @@ export class DistributedTransactionService {
this.logger.debug(`Rollback completed for step: ${stepId} [${transactionId}]`);
} catch (rollbackError) {
this.logger.error(`Rollback failed for step: ${stepId} [${transactionId}]`, {
error: getErrorMessage(rollbackError)
error: getErrorMessage(rollbackError),
});
// Continue with other rollbacks even if one fails
}

View File

@ -16,24 +16,24 @@ export interface TransactionOptions {
* Default: 30 seconds
*/
timeout?: number;
/**
* Maximum number of retry attempts on serialization failures
* Default: 3
*/
maxRetries?: number;
/**
* Custom isolation level for the transaction
* Default: ReadCommitted
*/
isolationLevel?: 'ReadUncommitted' | 'ReadCommitted' | 'RepeatableRead' | 'Serializable';
isolationLevel?: "ReadUncommitted" | "ReadCommitted" | "RepeatableRead" | "Serializable";
/**
* Description of the transaction for logging
*/
description?: string;
/**
* Whether to automatically rollback external operations on database rollback
* Default: true
@ -58,7 +58,7 @@ export interface TransactionResult<T> {
export class TransactionService {
private readonly defaultTimeout = 30000; // 30 seconds
private readonly defaultMaxRetries = 3;
constructor(
private readonly prisma: PrismaService,
@Inject(Logger) private readonly logger: Logger
@ -66,26 +66,26 @@ export class TransactionService {
/**
* Execute operations within a database transaction with rollback support
*
*
* @example
* ```typescript
* const result = await this.transactionService.executeTransaction(
* async (tx, context) => {
* // Database operations
* const user = await tx.user.create({ data: userData });
*
*
* // External operations with rollback
* const whmcsClient = await this.whmcsService.createClient(user.email);
* context.addRollback(async () => {
* await this.whmcsService.deleteClient(whmcsClient.id);
* });
*
* // Salesforce operations with rollback
*
* // Salesforce operations with rollback
* const sfAccount = await this.salesforceService.createAccount(user);
* context.addRollback(async () => {
* await this.salesforceService.deleteAccount(sfAccount.Id);
* });
*
*
* return { user, whmcsClient, sfAccount };
* },
* {
@ -102,26 +102,26 @@ export class TransactionService {
const {
timeout = this.defaultTimeout,
maxRetries = this.defaultMaxRetries,
isolationLevel = 'ReadCommitted',
description = 'Database transaction',
autoRollback = true
isolationLevel = "ReadCommitted",
description = "Database transaction",
autoRollback = true,
} = options;
const transactionId = this.generateTransactionId();
const startTime = new Date();
let context: TransactionContext = {
id: transactionId,
startTime,
operations: [],
rollbackActions: []
rollbackActions: [],
};
this.logger.log(`Starting transaction [${transactionId}]`, {
description,
timeout,
isolationLevel,
maxRetries
maxRetries,
});
let attempt = 0;
@ -129,7 +129,7 @@ export class TransactionService {
while (attempt < maxRetries) {
attempt++;
try {
// Reset context for retry attempts
if (attempt > 1) {
@ -137,22 +137,22 @@ export class TransactionService {
id: transactionId,
startTime,
operations: [],
rollbackActions: []
rollbackActions: [],
};
}
const result = await Promise.race([
this.executeTransactionAttempt(operation, context, isolationLevel),
this.createTimeoutPromise<T>(timeout, transactionId)
this.createTimeoutPromise<T>(timeout, transactionId),
]);
const duration = Date.now() - startTime.getTime();
this.logger.log(`Transaction completed successfully [${transactionId}]`, {
description,
duration,
attempt,
operationsCount: context.operations.length
operationsCount: context.operations.length,
});
return {
@ -160,31 +160,30 @@ export class TransactionService {
data: result,
duration,
operationsCount: context.operations.length,
rollbacksExecuted: 0
rollbacksExecuted: 0,
};
} catch (error) {
lastError = error as Error;
const duration = Date.now() - startTime.getTime();
this.logger.error(`Transaction attempt ${attempt} failed [${transactionId}]`, {
description,
error: getErrorMessage(error),
duration,
operationsCount: context.operations.length,
rollbackActionsCount: context.rollbackActions.length
rollbackActionsCount: context.rollbackActions.length,
});
// Execute rollbacks if this is the final attempt or not a retryable error
if (attempt === maxRetries || !this.isRetryableError(error)) {
const rollbacksExecuted = await this.executeRollbacks(context, autoRollback);
return {
success: false,
error: getErrorMessage(error),
duration,
operationsCount: context.operations.length,
rollbacksExecuted
rollbacksExecuted,
};
}
@ -197,10 +196,10 @@ export class TransactionService {
const duration = Date.now() - startTime.getTime();
return {
success: false,
error: lastError ? getErrorMessage(lastError) : 'Unknown transaction error',
error: lastError ? getErrorMessage(lastError) : "Unknown transaction error",
duration,
operationsCount: context.operations.length,
rollbacksExecuted: 0
rollbacksExecuted: 0,
};
}
@ -209,15 +208,15 @@ export class TransactionService {
*/
async executeSimpleTransaction<T>(
operation: (tx: any) => Promise<T>,
options: Omit<TransactionOptions, 'autoRollback'> = {}
options: Omit<TransactionOptions, "autoRollback"> = {}
): Promise<T> {
const result = await this.executeTransaction(
async (tx, _context) => operation(tx),
{ ...options, autoRollback: false }
);
const result = await this.executeTransaction(async (tx, _context) => operation(tx), {
...options,
autoRollback: false,
});
if (!result.success) {
throw new Error(result.error || 'Transaction failed');
throw new Error(result.error || "Transaction failed");
}
return result.data!;
@ -229,16 +228,16 @@ export class TransactionService {
isolationLevel: string
): Promise<T> {
return await this.prisma.$transaction(
async (tx) => {
async tx => {
// Enhance context with helper methods
const enhancedContext = this.enhanceContext(context);
// Execute the operation
return await operation(tx, enhancedContext);
},
{
isolationLevel: isolationLevel as any,
timeout: 30000 // Prisma transaction timeout
timeout: 30000, // Prisma transaction timeout
}
);
}
@ -251,7 +250,7 @@ export class TransactionService {
},
addRollback: (rollbackFn: () => Promise<void>) => {
context.rollbackActions.push(rollbackFn);
}
},
} as TransactionContext & {
addOperation: (description: string) => void;
addRollback: (rollbackFn: () => Promise<void>) => void;
@ -259,17 +258,19 @@ export class TransactionService {
}
private async executeRollbacks(
context: TransactionContext,
context: TransactionContext,
autoRollback: boolean
): Promise<number> {
if (!autoRollback || context.rollbackActions.length === 0) {
return 0;
}
this.logger.warn(`Executing ${context.rollbackActions.length} rollback actions [${context.id}]`);
this.logger.warn(
`Executing ${context.rollbackActions.length} rollback actions [${context.id}]`
);
let rollbacksExecuted = 0;
// Execute rollbacks in reverse order (LIFO)
for (let i = context.rollbackActions.length - 1; i >= 0; i--) {
try {
@ -278,26 +279,28 @@ export class TransactionService {
this.logger.debug(`Rollback ${i + 1} completed [${context.id}]`);
} catch (rollbackError) {
this.logger.error(`Rollback ${i + 1} failed [${context.id}]`, {
error: getErrorMessage(rollbackError)
error: getErrorMessage(rollbackError),
});
// Continue with other rollbacks even if one fails
}
}
this.logger.log(`Completed ${rollbacksExecuted}/${context.rollbackActions.length} rollbacks [${context.id}]`);
this.logger.log(
`Completed ${rollbacksExecuted}/${context.rollbackActions.length} rollbacks [${context.id}]`
);
return rollbacksExecuted;
}
private isRetryableError(error: unknown): boolean {
const errorMessage = getErrorMessage(error).toLowerCase();
// Retry on serialization failures, deadlocks, and temporary connection issues
return (
errorMessage.includes('serialization failure') ||
errorMessage.includes('deadlock') ||
errorMessage.includes('connection') ||
errorMessage.includes('timeout') ||
errorMessage.includes('lock wait timeout')
errorMessage.includes("serialization failure") ||
errorMessage.includes("deadlock") ||
errorMessage.includes("connection") ||
errorMessage.includes("timeout") ||
errorMessage.includes("lock wait timeout")
);
}
@ -326,7 +329,7 @@ export class TransactionService {
activeTransactions: 0, // Would need to track active transactions
totalTransactions: 0, // Would need to track total count
successRate: 0, // Would need to track success/failure rates
averageDuration: 0 // Would need to track durations
averageDuration: 0, // Would need to track durations
};
}
}

View File

@ -12,13 +12,13 @@ export class QueueHealthController {
) {}
@Get()
@ApiOperation({
@ApiOperation({
summary: "Get queue health status",
description: "Returns health status and metrics for WHMCS and Salesforce request queues"
description: "Returns health status and metrics for WHMCS and Salesforce request queues",
})
@ApiResponse({
status: 200,
description: "Queue health status retrieved successfully"
@ApiResponse({
status: 200,
description: "Queue health status retrieved successfully",
})
getQueueHealth() {
return {
@ -36,13 +36,13 @@ export class QueueHealthController {
}
@Get("whmcs")
@ApiOperation({
@ApiOperation({
summary: "Get WHMCS queue metrics",
description: "Returns detailed metrics for the WHMCS request queue"
description: "Returns detailed metrics for the WHMCS request queue",
})
@ApiResponse({
status: 200,
description: "WHMCS queue metrics retrieved successfully"
@ApiResponse({
status: 200,
description: "WHMCS queue metrics retrieved successfully",
})
getWhmcsQueueMetrics() {
return {
@ -53,13 +53,14 @@ export class QueueHealthController {
}
@Get("salesforce")
@ApiOperation({
@ApiOperation({
summary: "Get Salesforce queue metrics",
description: "Returns detailed metrics for the Salesforce request queue including daily API usage"
description:
"Returns detailed metrics for the Salesforce request queue including daily API usage",
})
@ApiResponse({
status: 200,
description: "Salesforce queue metrics retrieved successfully"
@ApiResponse({
status: 200,
description: "Salesforce queue metrics retrieved successfully",
})
getSalesforceQueueMetrics() {
return {

View File

@ -7,7 +7,6 @@ import {
Inject,
} from "@nestjs/common";
import { Request, Response } from "express";
import { getClientSafeErrorMessage } from "../utils/error.util";
import { Logger } from "nestjs-pino";
import { ConfigService } from "@nestjs/config";
import { SecureErrorMapperService } from "../security/services/secure-error-mapper.service";
@ -41,14 +40,15 @@ export class GlobalExceptionFilter implements ExceptionFilter {
// Determine HTTP status
if (exception instanceof HttpException) {
status = exception.getStatus();
// Extract the actual error from HttpException response
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === "object" && exceptionResponse !== null) {
const errorResponse = exceptionResponse as { message?: string; error?: string };
originalError = errorResponse.message || exception.message;
} else {
originalError = typeof exceptionResponse === "string" ? exceptionResponse : exception.message;
originalError =
typeof exceptionResponse === "string" ? exceptionResponse : exception.message;
}
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
@ -58,11 +58,11 @@ export class GlobalExceptionFilter implements ExceptionFilter {
// Use secure error mapper to get safe public message and log securely
const errorClassification = this.secureErrorMapper.mapError(originalError, errorContext);
const publicMessage = this.secureErrorMapper.getPublicMessage(originalError, errorContext);
// Log the error securely (this handles sensitive data filtering)
this.secureErrorMapper.logSecureError(originalError, errorContext, {
httpStatus: status,
exceptionType: exception instanceof Error ? exception.constructor.name : 'Unknown'
exceptionType: exception instanceof Error ? exception.constructor.name : "Unknown",
});
// Create secure error response

View File

@ -25,13 +25,13 @@ export interface SalesforceRequestOptions {
/**
* Salesforce Request Queue Service
*
*
* Manages concurrent requests to Salesforce API to prevent:
* - Daily API limit exhaustion (100,000 + 1,000 per user)
* - Concurrent request limit violations (25 long-running requests)
* - Rate limit violations and 429 errors
* - Optimal resource utilization
*
*
* Based on Salesforce documentation:
* - Daily limit: 100,000 + (1,000 × users) per 24h
* - Concurrent limit: 25 long-running requests (>20s)
@ -110,10 +110,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
// Wait for pending requests to complete (with timeout)
try {
await Promise.all([
this.standardQueue.onIdle(),
this.longRunningQueue.onIdle(),
]);
await Promise.all([this.standardQueue.onIdle(), this.longRunningQueue.onIdle()]);
} catch (error) {
this.logger.warn("Some Salesforce requests may not have completed during shutdown", {
error: error instanceof Error ? error.message : String(error),
@ -157,10 +154,10 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
this.recordWaitTime(waitTime);
const executionStart = Date.now();
try {
const response = await this.executeWithRetry(requestFn, options);
const executionTime = Date.now() - executionStart;
this.recordExecutionTime(executionTime);
this.metrics.completedRequests++;
@ -219,10 +216,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
/**
* Execute high-priority Salesforce request (jumps queue)
*/
async executeHighPriority<T>(
requestFn: () => Promise<T>,
isLongRunning = false
): Promise<T> {
async executeHighPriority<T>(requestFn: () => Promise<T>, isLongRunning = false): Promise<T> {
return this.execute(requestFn, { priority: 10, isLongRunning });
}
@ -253,29 +247,20 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
averageWaitTime: number;
} {
this.updateQueueMetrics();
const errorRate = this.metrics.totalRequests > 0
? this.metrics.failedRequests / this.metrics.totalRequests
: 0;
const errorRate =
this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
// Estimate daily limit (conservative: 150,000 for ~50 users)
const estimatedDailyLimit = 150000;
const dailyUsagePercent = this.metrics.dailyApiUsage / estimatedDailyLimit;
let status: "healthy" | "degraded" | "unhealthy" = "healthy";
// Adjusted thresholds for higher throughput (15 concurrent, 10 RPS)
if (
this.metrics.queueSize > 200 ||
errorRate > 0.1 ||
dailyUsagePercent > 0.9
) {
if (this.metrics.queueSize > 200 || errorRate > 0.1 || dailyUsagePercent > 0.9) {
status = "unhealthy";
} else if (
this.metrics.queueSize > 80 ||
errorRate > 0.05 ||
dailyUsagePercent > 0.7
) {
} else if (this.metrics.queueSize > 80 || errorRate > 0.05 || dailyUsagePercent > 0.7) {
status = "degraded";
}
@ -319,11 +304,8 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
this.standardQueue.clear();
this.longRunningQueue.clear();
await Promise.all([
this.standardQueue.onIdle(),
this.longRunningQueue.onIdle(),
]);
await Promise.all([this.standardQueue.onIdle(), this.longRunningQueue.onIdle()]);
}
private async executeWithRetry<T>(
@ -347,7 +329,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
// Special handling for rate limit errors
let delay = baseDelay * Math.pow(2, attempt - 1);
if (this.isRateLimitError(error)) {
// Longer delay for rate limit errors
delay = Math.max(delay, 30000); // At least 30 seconds
@ -355,7 +337,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
// Add jitter
delay += Math.random() * 1000;
this.logger.debug("Salesforce request failed, retrying", {
attempt,
maxAttempts,
@ -386,12 +368,12 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
private checkDailyUsage(): void {
const now = new Date();
// Reset daily usage if we've passed the reset time
if (now >= this.dailyUsageResetTime) {
this.metrics.dailyApiUsage = 0;
this.dailyUsageResetTime = this.getNextDayReset();
this.logger.log("Daily Salesforce API usage reset", {
resetTime: this.dailyUsageResetTime,
});
@ -436,15 +418,15 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
private updateQueueMetrics(): void {
this.metrics.queueSize = this.standardQueue.size + this.longRunningQueue.size;
this.metrics.pendingRequests = this.standardQueue.pending + this.longRunningQueue.pending;
// Calculate averages
if (this.waitTimes.length > 0) {
this.metrics.averageWaitTime =
this.metrics.averageWaitTime =
this.waitTimes.reduce((sum, time) => sum + time, 0) / this.waitTimes.length;
}
if (this.executionTimes.length > 0) {
this.metrics.averageExecutionTime =
this.metrics.averageExecutionTime =
this.executionTimes.reduce((sum, time) => sum + time, 0) / this.executionTimes.length;
}
}

View File

@ -22,13 +22,13 @@ export interface WhmcsRequestOptions {
/**
* WHMCS Request Queue Service
*
*
* Manages concurrent requests to WHMCS API to prevent:
* - Database connection pool exhaustion
* - Server overload from parallel requests
* - Rate limit violations (conservative approach)
* - Resource contention issues
*
*
* Based on research:
* - WHMCS has no official rate limits but performance degrades with high concurrency
* - Conservative approach: max 3 concurrent requests
@ -56,7 +56,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
private async initializeQueue() {
if (!this.queue) {
const { default: PQueue } = await import("p-queue");
// Optimized WHMCS queue configuration for better user experience
this.queue = new PQueue({
concurrency: 15, // Max 15 concurrent WHMCS requests (matches Salesforce)
@ -100,10 +100,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
/**
* Execute a WHMCS API request through the queue
*/
async execute<T>(
requestFn: () => Promise<T>,
options: WhmcsRequestOptions = {}
): Promise<T> {
async execute<T>(requestFn: () => Promise<T>, options: WhmcsRequestOptions = {}): Promise<T> {
await this.initializeQueue();
const startTime = Date.now();
const requestId = this.generateRequestId();
@ -125,10 +122,10 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
this.recordWaitTime(waitTime);
const executionStart = Date.now();
try {
const response = await this.executeWithRetry(requestFn, options);
const executionTime = Date.now() - executionStart;
this.recordExecutionTime(executionTime);
this.metrics.completedRequests++;
@ -199,13 +196,12 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
averageWaitTime: number;
} {
this.updateQueueMetrics();
const errorRate = this.metrics.totalRequests > 0
? this.metrics.failedRequests / this.metrics.totalRequests
: 0;
const errorRate =
this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
let status: "healthy" | "degraded" | "unhealthy" = "healthy";
// Adjusted thresholds for higher throughput (15 concurrent, 5 RPS)
if (this.metrics.queueSize > 120 || errorRate > 0.1) {
status = "unhealthy";
@ -256,7 +252,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000;
this.logger.debug("WHMCS request failed, retrying", {
attempt,
maxAttempts,
@ -295,15 +291,15 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
private updateQueueMetrics(): void {
this.metrics.queueSize = this.queue.size;
this.metrics.pendingRequests = this.queue.pending;
// Calculate averages
if (this.waitTimes.length > 0) {
this.metrics.averageWaitTime =
this.metrics.averageWaitTime =
this.waitTimes.reduce((sum, time) => sum + time, 0) / this.waitTimes.length;
}
if (this.executionTimes.length > 0) {
this.metrics.averageExecutionTime =
this.metrics.averageExecutionTime =
this.executionTimes.reduce((sum, time) => sum + time, 0) / this.executionTimes.length;
}
}

View File

@ -1,4 +1,4 @@
import { Controller, Get, Post, Req, Res, UseGuards, Inject } from "@nestjs/common";
import { Controller, Get, Post, Req, Res, Inject } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from "@nestjs/swagger";
import type { Request, Response } from "express";
import { Logger } from "nestjs-pino";
@ -8,30 +8,30 @@ interface AuthenticatedRequest extends Request {
user?: { id: string; sessionId?: string };
}
@ApiTags('Security')
@Controller('security/csrf')
@ApiTags("Security")
@Controller("security/csrf")
export class CsrfController {
constructor(
private readonly csrfService: CsrfService,
@Inject(Logger) private readonly logger: Logger
) {}
@Get('token')
@ApiOperation({
summary: 'Get CSRF token',
description: 'Generates and returns a new CSRF token for the current session'
@Get("token")
@ApiOperation({
summary: "Get CSRF token",
description: "Generates and returns a new CSRF token for the current session",
})
@ApiResponse({
status: 200,
description: 'CSRF token generated successfully',
@ApiResponse({
status: 200,
description: "CSRF token generated successfully",
schema: {
type: 'object',
type: "object",
properties: {
success: { type: 'boolean', example: true },
token: { type: 'string', example: 'abc123...' },
expiresAt: { type: 'string', format: 'date-time' }
}
}
success: { type: "boolean", example: true },
token: { type: "string", example: "abc123..." },
expiresAt: { type: "string", format: "date-time" },
},
},
})
getCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) {
const sessionId = req.user?.sessionId || this.extractSessionId(req) || undefined;
@ -41,49 +41,49 @@ export class CsrfController {
const tokenData = this.csrfService.generateToken(sessionId, userId);
// Set CSRF secret in secure cookie
res.cookie('csrf-secret', tokenData.secret, {
res.cookie("csrf-secret", tokenData.secret, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 3600000, // 1 hour
path: '/',
path: "/",
});
this.logger.debug("CSRF token requested", {
userId,
sessionId,
userAgent: req.get('user-agent'),
ip: req.ip
userAgent: req.get("user-agent"),
ip: req.ip,
});
return res.json({
success: true,
token: tokenData.token,
expiresAt: tokenData.expiresAt.toISOString()
expiresAt: tokenData.expiresAt.toISOString(),
});
}
@Post('refresh')
@Post("refresh")
@ApiBearerAuth()
@ApiOperation({
summary: 'Refresh CSRF token',
description: 'Invalidates current token and generates a new one for authenticated users'
@ApiOperation({
summary: "Refresh CSRF token",
description: "Invalidates current token and generates a new one for authenticated users",
})
@ApiResponse({
status: 200,
description: 'CSRF token refreshed successfully',
@ApiResponse({
status: 200,
description: "CSRF token refreshed successfully",
schema: {
type: 'object',
type: "object",
properties: {
success: { type: 'boolean', example: true },
token: { type: 'string', example: 'xyz789...' },
expiresAt: { type: 'string', format: 'date-time' }
}
}
success: { type: "boolean", example: true },
token: { type: "string", example: "xyz789..." },
expiresAt: { type: "string", format: "date-time" },
},
},
})
refreshCsrfToken(@Req() req: AuthenticatedRequest, @Res() res: Response) {
const sessionId = req.user?.sessionId || this.extractSessionId(req) || undefined;
const userId = req.user?.id || 'anonymous'; // Default for unauthenticated users
const userId = req.user?.id || "anonymous"; // Default for unauthenticated users
// Invalidate existing tokens for this user
this.csrfService.invalidateUserTokens(userId);
@ -92,76 +92,75 @@ export class CsrfController {
const tokenData = this.csrfService.generateToken(sessionId, userId);
// Set CSRF secret in secure cookie
res.cookie('csrf-secret', tokenData.secret, {
res.cookie("csrf-secret", tokenData.secret, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 3600000, // 1 hour
path: '/',
path: "/",
});
this.logger.debug("CSRF token refreshed", {
userId,
sessionId,
userAgent: req.get('user-agent'),
ip: req.ip
userAgent: req.get("user-agent"),
ip: req.ip,
});
return res.json({
success: true,
token: tokenData.token,
expiresAt: tokenData.expiresAt.toISOString()
expiresAt: tokenData.expiresAt.toISOString(),
});
}
@Get('stats')
@Get("stats")
@ApiBearerAuth()
@ApiOperation({
summary: 'Get CSRF token statistics',
description: 'Returns statistics about CSRF tokens (admin/monitoring endpoint)'
@ApiOperation({
summary: "Get CSRF token statistics",
description: "Returns statistics about CSRF tokens (admin/monitoring endpoint)",
})
@ApiResponse({
status: 200,
description: 'CSRF token statistics',
@ApiResponse({
status: 200,
description: "CSRF token statistics",
schema: {
type: 'object',
type: "object",
properties: {
success: { type: 'boolean', example: true },
success: { type: "boolean", example: true },
stats: {
type: 'object',
type: "object",
properties: {
totalTokens: { type: 'number', example: 150 },
activeTokens: { type: 'number', example: 120 },
expiredTokens: { type: 'number', example: 30 },
cacheSize: { type: 'number', example: 150 },
maxCacheSize: { type: 'number', example: 10000 }
}
}
}
}
totalTokens: { type: "number", example: 150 },
activeTokens: { type: "number", example: 120 },
expiredTokens: { type: "number", example: 30 },
cacheSize: { type: "number", example: 150 },
maxCacheSize: { type: "number", example: 10000 },
},
},
},
},
})
getCsrfStats(@Req() req: AuthenticatedRequest) {
const userId = req.user?.id || 'anonymous';
const userId = req.user?.id || "anonymous";
// Only allow admin users to see stats (you might want to add role checking)
this.logger.debug("CSRF stats requested", {
userId,
userAgent: req.get('user-agent'),
ip: req.ip
userAgent: req.get("user-agent"),
ip: req.ip,
});
const stats = this.csrfService.getTokenStats();
return {
success: true,
stats
stats,
};
}
private extractSessionId(req: AuthenticatedRequest): string | null {
return req.cookies?.['session-id'] ||
req.cookies?.['connect.sid'] ||
(req as any).sessionID ||
null;
return (
req.cookies?.["session-id"] || req.cookies?.["connect.sid"] || (req as any).sessionID || null
);
}
}

View File

@ -26,19 +26,19 @@ export class CsrfMiddleware implements NestMiddleware {
@Inject(Logger) private readonly logger: Logger
) {
this.isProduction = this.configService.get("NODE_ENV") === "production";
// Paths that don't require CSRF protection
this.exemptPaths = new Set([
'/api/auth/login',
'/api/auth/signup',
'/api/auth/refresh',
'/api/health',
'/docs',
'/api/webhooks', // Webhooks typically don't use CSRF
"/api/auth/login",
"/api/auth/signup",
"/api/auth/refresh",
"/api/health",
"/docs",
"/api/webhooks", // Webhooks typically don't use CSRF
]);
// Methods that don't require CSRF protection (safe methods)
this.exemptMethods = new Set(['GET', 'HEAD', 'OPTIONS']);
this.exemptMethods = new Set(["GET", "HEAD", "OPTIONS"]);
}
use(req: CsrfRequest, res: Response, next: NextFunction): void {
@ -68,7 +68,7 @@ export class CsrfMiddleware implements NestMiddleware {
}
// Check for API endpoints that might be exempt
if (req.path.startsWith('/api/webhooks/')) {
if (req.path.startsWith("/api/webhooks/")) {
return true;
}
@ -77,7 +77,7 @@ export class CsrfMiddleware implements NestMiddleware {
private requiresCsrfProtection(req: CsrfRequest): boolean {
// State-changing methods require CSRF protection
return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method);
return ["POST", "PUT", "PATCH", "DELETE"].includes(req.method);
}
private validateCsrfToken(req: CsrfRequest, res: Response, next: NextFunction): void {
@ -90,8 +90,8 @@ export class CsrfMiddleware implements NestMiddleware {
this.logger.warn("CSRF validation failed - missing token", {
method: req.method,
path: req.path,
userAgent: req.get('user-agent'),
ip: req.ip
userAgent: req.get("user-agent"),
ip: req.ip,
});
throw new ForbiddenException("CSRF token required");
}
@ -100,23 +100,28 @@ export class CsrfMiddleware implements NestMiddleware {
this.logger.warn("CSRF validation failed - missing secret cookie", {
method: req.method,
path: req.path,
userAgent: req.get('user-agent'),
ip: req.ip
userAgent: req.get("user-agent"),
ip: req.ip,
});
throw new ForbiddenException("CSRF secret required");
}
const validationResult = this.csrfService.validateToken(token, secret, sessionId || undefined, userId);
const validationResult = this.csrfService.validateToken(
token,
secret,
sessionId || undefined,
userId
);
if (!validationResult.isValid) {
this.logger.warn("CSRF validation failed", {
reason: validationResult.reason,
method: req.method,
path: req.path,
userAgent: req.get('user-agent'),
userAgent: req.get("user-agent"),
ip: req.ip,
userId,
sessionId
sessionId,
});
throw new ForbiddenException(`CSRF validation failed: ${validationResult.reason}`);
}
@ -128,7 +133,7 @@ export class CsrfMiddleware implements NestMiddleware {
method: req.method,
path: req.path,
userId,
sessionId
sessionId,
});
next();
@ -151,13 +156,13 @@ export class CsrfMiddleware implements NestMiddleware {
this.setCsrfSecretCookie(res, tokenData.secret);
// Set CSRF token in response header for client to use
res.setHeader('X-CSRF-Token', tokenData.token);
res.setHeader("X-CSRF-Token", tokenData.token);
this.logger.debug("CSRF token generated and set", {
method: req.method,
path: req.path,
userId,
sessionId
sessionId,
});
next();
@ -165,30 +170,30 @@ export class CsrfMiddleware implements NestMiddleware {
private extractTokenFromRequest(req: CsrfRequest): string | null {
// Check multiple possible locations for the CSRF token
// 1. X-CSRF-Token header (most common)
let token = req.get('X-CSRF-Token');
let token = req.get("X-CSRF-Token");
if (token) return token;
// 2. X-Requested-With header (alternative)
token = req.get('X-Requested-With');
if (token && token !== 'XMLHttpRequest') return token;
token = req.get("X-Requested-With");
if (token && token !== "XMLHttpRequest") return token;
// 3. Authorization header (if using Bearer token pattern)
const authHeader = req.get('Authorization');
if (authHeader && authHeader.startsWith('CSRF ')) {
const authHeader = req.get("Authorization");
if (authHeader && authHeader.startsWith("CSRF ")) {
return authHeader.substring(5);
}
// 4. Request body (for form submissions)
if (req.body && typeof req.body === 'object') {
if (req.body && typeof req.body === "object") {
token = req.body._csrf || req.body.csrfToken;
if (token) return token;
}
// 5. Query parameter (least secure, only for GET requests)
if (req.method === 'GET') {
token = req.query._csrf as string || req.query.csrfToken as string;
if (req.method === "GET") {
token = (req.query._csrf as string) || (req.query.csrfToken as string);
if (token) return token;
}
@ -196,26 +201,23 @@ export class CsrfMiddleware implements NestMiddleware {
}
private extractSecretFromCookie(req: CsrfRequest): string | null {
return req.cookies?.['csrf-secret'] || null;
return req.cookies?.["csrf-secret"] || null;
}
private extractSessionId(req: CsrfRequest): string | null {
// Try to extract session ID from various sources
return req.cookies?.['session-id'] ||
req.cookies?.['connect.sid'] ||
req.sessionID ||
null;
return req.cookies?.["session-id"] || req.cookies?.["connect.sid"] || req.sessionID || null;
}
private setCsrfSecretCookie(res: Response, secret: string): void {
const cookieOptions = {
httpOnly: true,
secure: this.isProduction,
sameSite: 'strict' as const,
sameSite: "strict" as const,
maxAge: 3600000, // 1 hour
path: '/',
path: "/",
};
res.cookie('csrf-secret', secret, cookieOptions);
res.cookie("csrf-secret", secret, cookieOptions);
}
}

View File

@ -14,8 +14,6 @@ import { CsrfController } from "./controllers/csrf.controller";
export class SecurityModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// Apply CSRF middleware to all routes except those handled by the middleware itself
consumer
.apply(CsrfMiddleware)
.forRoutes('*');
consumer.apply(CsrfMiddleware).forRoutes("*");
}
}

View File

@ -34,9 +34,11 @@ export class CsrfService {
) {
this.tokenExpiry = Number(this.configService.get("CSRF_TOKEN_EXPIRY", "3600000")); // 1 hour default
this.secretKey = this.configService.get("CSRF_SECRET_KEY") || this.generateSecretKey();
if (!this.configService.get("CSRF_SECRET_KEY")) {
this.logger.warn("CSRF_SECRET_KEY not configured, using generated key (not suitable for production)");
this.logger.warn(
"CSRF_SECRET_KEY not configured, using generated key (not suitable for production)"
);
}
// Clean up expired tokens periodically
@ -56,12 +58,12 @@ export class CsrfService {
secret,
expiresAt,
sessionId,
userId
userId,
};
// Store in cache for validation
this.tokenCache.set(token, tokenData);
// Prevent memory leaks
if (this.tokenCache.size > this.maxCacheSize) {
this.cleanupExpiredTokens();
@ -71,7 +73,7 @@ export class CsrfService {
tokenHash: this.hashToken(token),
sessionId,
userId,
expiresAt: expiresAt.toISOString()
expiresAt: expiresAt.toISOString(),
});
return tokenData;
@ -81,15 +83,15 @@ export class CsrfService {
* Validate a CSRF token against the provided secret
*/
validateToken(
token: string,
secret: string,
sessionId?: string,
token: string,
secret: string,
sessionId?: string,
userId?: string
): CsrfValidationResult {
if (!token || !secret) {
return {
isValid: false,
reason: "Missing token or secret"
reason: "Missing token or secret",
};
}
@ -98,7 +100,7 @@ export class CsrfService {
if (!cachedTokenData) {
return {
isValid: false,
reason: "Token not found or expired"
reason: "Token not found or expired",
};
}
@ -107,7 +109,7 @@ export class CsrfService {
this.tokenCache.delete(token);
return {
isValid: false,
reason: "Token expired"
reason: "Token expired",
};
}
@ -116,11 +118,11 @@ export class CsrfService {
this.logger.warn("CSRF token validation failed - secret mismatch", {
tokenHash: this.hashToken(token),
sessionId,
userId
userId,
});
return {
isValid: false,
reason: "Invalid secret"
reason: "Invalid secret",
};
}
@ -129,11 +131,11 @@ export class CsrfService {
this.logger.warn("CSRF token validation failed - session mismatch", {
tokenHash: this.hashToken(token),
expectedSession: cachedTokenData.sessionId,
providedSession: sessionId
providedSession: sessionId,
});
return {
isValid: false,
reason: "Session mismatch"
reason: "Session mismatch",
};
}
@ -142,18 +144,18 @@ export class CsrfService {
this.logger.warn("CSRF token validation failed - user mismatch", {
tokenHash: this.hashToken(token),
expectedUser: cachedTokenData.userId,
providedUser: userId
providedUser: userId,
});
return {
isValid: false,
reason: "User mismatch"
reason: "User mismatch",
};
}
// Regenerate expected token to prevent timing attacks
const expectedToken = this.generateTokenFromSecret(
cachedTokenData.secret,
cachedTokenData.sessionId,
cachedTokenData.secret,
cachedTokenData.sessionId,
cachedTokenData.userId
);
@ -162,23 +164,23 @@ export class CsrfService {
this.logger.warn("CSRF token validation failed - token mismatch", {
tokenHash: this.hashToken(token),
sessionId,
userId
userId,
});
return {
isValid: false,
reason: "Invalid token"
reason: "Invalid token",
};
}
this.logger.debug("CSRF token validated successfully", {
tokenHash: this.hashToken(token),
sessionId,
userId
userId,
});
return {
isValid: true,
tokenData: cachedTokenData
tokenData: cachedTokenData,
};
}
@ -188,7 +190,7 @@ export class CsrfService {
invalidateToken(token: string): void {
this.tokenCache.delete(token);
this.logger.debug("CSRF token invalidated", {
tokenHash: this.hashToken(token)
tokenHash: this.hashToken(token),
});
}
@ -203,10 +205,10 @@ export class CsrfService {
invalidatedCount++;
}
}
this.logger.debug("CSRF tokens invalidated for session", {
sessionId,
invalidatedCount
invalidatedCount,
});
}
@ -221,10 +223,10 @@ export class CsrfService {
invalidatedCount++;
}
}
this.logger.debug("CSRF tokens invalidated for user", {
userId,
invalidatedCount
invalidatedCount,
});
}
@ -249,30 +251,32 @@ export class CsrfService {
activeTokens,
expiredTokens,
cacheSize: this.tokenCache.size,
maxCacheSize: this.maxCacheSize
maxCacheSize: this.maxCacheSize,
};
}
private generateSecret(): string {
return crypto.randomBytes(32).toString('base64url');
return crypto.randomBytes(32).toString("base64url");
}
private generateTokenFromSecret(secret: string, sessionId?: string, userId?: string): string {
const data = [secret, sessionId || '', userId || ''].join('|');
const hmac = crypto.createHmac('sha256', this.secretKey);
const data = [secret, sessionId || "", userId || ""].join("|");
const hmac = crypto.createHmac("sha256", this.secretKey);
hmac.update(data);
return hmac.digest('base64url');
return hmac.digest("base64url");
}
private generateSecretKey(): string {
const key = crypto.randomBytes(64).toString('base64url');
this.logger.warn("Generated CSRF secret key - set CSRF_SECRET_KEY environment variable for production");
const key = crypto.randomBytes(64).toString("base64url");
this.logger.warn(
"Generated CSRF secret key - set CSRF_SECRET_KEY environment variable for production"
);
return key;
}
private hashToken(token: string): string {
// Create a hash of the token for logging (never log the actual token)
return crypto.createHash('sha256').update(token).digest('hex').substring(0, 16);
return crypto.createHash("sha256").update(token).digest("hex").substring(0, 16);
}
private constantTimeEquals(a: string, b: string): boolean {
@ -302,7 +306,7 @@ export class CsrfService {
if (cleanedCount > 0) {
this.logger.debug("Cleaned up expired CSRF tokens", {
cleanedCount,
remainingTokens: this.tokenCache.size
remainingTokens: this.tokenCache.size,
});
}
}

View File

@ -14,13 +14,13 @@ export interface ErrorContext {
export interface SecureErrorMapping {
code: string;
publicMessage: string;
logLevel: 'error' | 'warn' | 'info' | 'debug';
logLevel: "error" | "warn" | "info" | "debug";
shouldAlert?: boolean; // Whether to send alerts to monitoring
}
export interface ErrorClassification {
category: 'authentication' | 'authorization' | 'validation' | 'business' | 'system' | 'external';
severity: 'low' | 'medium' | 'high' | 'critical';
category: "authentication" | "authorization" | "validation" | "business" | "system" | "external";
severity: "low" | "medium" | "high" | "critical";
mapping: SecureErrorMapping;
}
@ -46,13 +46,10 @@ export class SecureErrorMapperService {
/**
* Map an error to a secure public message
*/
mapError(
error: unknown,
context?: ErrorContext
): ErrorClassification {
mapError(error: unknown, context?: ErrorContext): ErrorClassification {
const errorMessage = this.extractErrorMessage(error);
const errorCode = this.extractErrorCode(error);
// Try exact code mapping first
if (errorCode && this.errorMappings.has(errorCode)) {
const mapping = this.errorMappings.get(errorCode)!;
@ -76,7 +73,7 @@ export class SecureErrorMapperService {
*/
getPublicMessage(error: unknown, context?: ErrorContext): string {
const classification = this.mapError(error, context);
// In development, show more details
if (this.isDevelopment) {
const originalMessage = this.extractErrorMessage(error);
@ -90,13 +87,13 @@ export class SecureErrorMapperService {
* Log error with appropriate security level
*/
logSecureError(
error: unknown,
error: unknown,
context?: ErrorContext,
additionalData?: Record<string, unknown>
): void {
const classification = this.mapError(error, context);
const originalMessage = this.extractErrorMessage(error);
const logData = {
errorCode: classification.mapping.code,
category: classification.category,
@ -104,27 +101,27 @@ export class SecureErrorMapperService {
publicMessage: classification.mapping.publicMessage,
originalMessage: this.sanitizeForLogging(originalMessage),
context,
...additionalData
...additionalData,
};
// Log based on severity and log level
switch (classification.mapping.logLevel) {
case 'error':
case "error":
this.logger.error(`Security Error: ${classification.mapping.code}`, logData);
break;
case 'warn':
case "warn":
this.logger.warn(`Security Warning: ${classification.mapping.code}`, logData);
break;
case 'info':
case "info":
this.logger.log(`Security Info: ${classification.mapping.code}`, logData);
break;
case 'debug':
case "debug":
this.logger.debug(`Security Debug: ${classification.mapping.code}`, logData);
break;
}
// Send alerts for critical errors
if (classification.mapping.shouldAlert && classification.severity === 'critical') {
if (classification.mapping.shouldAlert && classification.severity === "critical") {
this.sendSecurityAlert(classification, context, logData);
}
}
@ -132,101 +129,149 @@ export class SecureErrorMapperService {
private initializeErrorMappings(): Map<string, SecureErrorMapping> {
return new Map([
// Authentication Errors
['INVALID_CREDENTIALS', {
code: 'AUTH_001',
publicMessage: 'Invalid email or password',
logLevel: 'warn'
}],
['ACCOUNT_LOCKED', {
code: 'AUTH_002',
publicMessage: 'Account temporarily locked. Please try again later',
logLevel: 'warn'
}],
['TOKEN_EXPIRED', {
code: 'AUTH_003',
publicMessage: 'Session expired. Please log in again',
logLevel: 'info'
}],
['TOKEN_INVALID', {
code: 'AUTH_004',
publicMessage: 'Invalid session. Please log in again',
logLevel: 'warn'
}],
[
"INVALID_CREDENTIALS",
{
code: "AUTH_001",
publicMessage: "Invalid email or password",
logLevel: "warn",
},
],
[
"ACCOUNT_LOCKED",
{
code: "AUTH_002",
publicMessage: "Account temporarily locked. Please try again later",
logLevel: "warn",
},
],
[
"TOKEN_EXPIRED",
{
code: "AUTH_003",
publicMessage: "Session expired. Please log in again",
logLevel: "info",
},
],
[
"TOKEN_INVALID",
{
code: "AUTH_004",
publicMessage: "Invalid session. Please log in again",
logLevel: "warn",
},
],
// Authorization Errors
['INSUFFICIENT_PERMISSIONS', {
code: 'AUTHZ_001',
publicMessage: 'You do not have permission to perform this action',
logLevel: 'warn'
}],
['RESOURCE_NOT_FOUND', {
code: 'AUTHZ_002',
publicMessage: 'The requested resource was not found',
logLevel: 'info'
}],
// Authorization Errors
[
"INSUFFICIENT_PERMISSIONS",
{
code: "AUTHZ_001",
publicMessage: "You do not have permission to perform this action",
logLevel: "warn",
},
],
[
"RESOURCE_NOT_FOUND",
{
code: "AUTHZ_002",
publicMessage: "The requested resource was not found",
logLevel: "info",
},
],
// Validation Errors
['VALIDATION_FAILED', {
code: 'VAL_001',
publicMessage: 'The provided data is invalid',
logLevel: 'info'
}],
['REQUIRED_FIELD_MISSING', {
code: 'VAL_002',
publicMessage: 'Required information is missing',
logLevel: 'info'
}],
[
"VALIDATION_FAILED",
{
code: "VAL_001",
publicMessage: "The provided data is invalid",
logLevel: "info",
},
],
[
"REQUIRED_FIELD_MISSING",
{
code: "VAL_002",
publicMessage: "Required information is missing",
logLevel: "info",
},
],
// Business Logic Errors
['ORDER_ALREADY_PROCESSED', {
code: 'BIZ_001',
publicMessage: 'This order has already been processed',
logLevel: 'info'
}],
['INSUFFICIENT_BALANCE', {
code: 'BIZ_002',
publicMessage: 'Insufficient account balance',
logLevel: 'info'
}],
['SERVICE_UNAVAILABLE', {
code: 'BIZ_003',
publicMessage: 'Service is temporarily unavailable',
logLevel: 'warn'
}],
[
"ORDER_ALREADY_PROCESSED",
{
code: "BIZ_001",
publicMessage: "This order has already been processed",
logLevel: "info",
},
],
[
"INSUFFICIENT_BALANCE",
{
code: "BIZ_002",
publicMessage: "Insufficient account balance",
logLevel: "info",
},
],
[
"SERVICE_UNAVAILABLE",
{
code: "BIZ_003",
publicMessage: "Service is temporarily unavailable",
logLevel: "warn",
},
],
// System Errors (High Security)
['DATABASE_ERROR', {
code: 'SYS_001',
publicMessage: 'A system error occurred. Please try again later',
logLevel: 'error',
shouldAlert: true
}],
['EXTERNAL_SERVICE_ERROR', {
code: 'SYS_002',
publicMessage: 'External service temporarily unavailable',
logLevel: 'error'
}],
['CONFIGURATION_ERROR', {
code: 'SYS_003',
publicMessage: 'System configuration error',
logLevel: 'error',
shouldAlert: true
}],
[
"DATABASE_ERROR",
{
code: "SYS_001",
publicMessage: "A system error occurred. Please try again later",
logLevel: "error",
shouldAlert: true,
},
],
[
"EXTERNAL_SERVICE_ERROR",
{
code: "SYS_002",
publicMessage: "External service temporarily unavailable",
logLevel: "error",
},
],
[
"CONFIGURATION_ERROR",
{
code: "SYS_003",
publicMessage: "System configuration error",
logLevel: "error",
shouldAlert: true,
},
],
// Rate Limiting
['RATE_LIMIT_EXCEEDED', {
code: 'RATE_001',
publicMessage: 'Too many requests. Please try again later',
logLevel: 'warn'
}],
[
"RATE_LIMIT_EXCEEDED",
{
code: "RATE_001",
publicMessage: "Too many requests. Please try again later",
logLevel: "warn",
},
],
// Generic Fallbacks
['UNKNOWN_ERROR', {
code: 'GEN_001',
publicMessage: 'An unexpected error occurred',
logLevel: 'error',
shouldAlert: true
}]
[
"UNKNOWN_ERROR",
{
code: "GEN_001",
publicMessage: "An unexpected error occurred",
logLevel: "error",
shouldAlert: true,
},
],
]);
}
@ -236,82 +281,82 @@ export class SecureErrorMapperService {
{
pattern: /database|connection|sql|prisma|postgres/i,
mapping: {
code: 'SYS_001',
publicMessage: 'A system error occurred. Please try again later',
logLevel: 'error',
shouldAlert: true
}
code: "SYS_001",
publicMessage: "A system error occurred. Please try again later",
logLevel: "error",
shouldAlert: true,
},
},
// Authentication patterns
{
pattern: /password|credential|token|secret|key|auth/i,
mapping: {
code: 'AUTH_001',
publicMessage: 'Authentication failed',
logLevel: 'warn'
}
code: "AUTH_001",
publicMessage: "Authentication failed",
logLevel: "warn",
},
},
// File system patterns
{
pattern: /file|path|directory|permission denied|enoent|eacces/i,
mapping: {
code: 'SYS_002',
publicMessage: 'System resource error',
logLevel: 'error',
shouldAlert: true
}
code: "SYS_002",
publicMessage: "System resource error",
logLevel: "error",
shouldAlert: true,
},
},
// Network/External service patterns
{
pattern: /network|timeout|connection refused|econnrefused|whmcs|salesforce/i,
mapping: {
code: 'SYS_002',
publicMessage: 'External service temporarily unavailable',
logLevel: 'error'
}
code: "SYS_002",
publicMessage: "External service temporarily unavailable",
logLevel: "error",
},
},
// Stack trace patterns
{
pattern: /\s+at\s+|\.js:\d+|\.ts:\d+|stack trace/i,
mapping: {
code: 'SYS_001',
publicMessage: 'A system error occurred. Please try again later',
logLevel: 'error',
shouldAlert: true
}
code: "SYS_001",
publicMessage: "A system error occurred. Please try again later",
logLevel: "error",
shouldAlert: true,
},
},
// Memory/Resource patterns
{
pattern: /memory|heap|out of memory|resource|limit exceeded/i,
mapping: {
code: 'SYS_003',
publicMessage: 'System resources temporarily unavailable',
logLevel: 'error',
shouldAlert: true
}
code: "SYS_003",
publicMessage: "System resources temporarily unavailable",
logLevel: "error",
shouldAlert: true,
},
},
// Validation patterns
{
pattern: /invalid|required|missing|validation|format/i,
mapping: {
code: 'VAL_001',
publicMessage: 'The provided data is invalid',
logLevel: 'info'
}
}
code: "VAL_001",
publicMessage: "The provided data is invalid",
logLevel: "info",
},
},
];
}
private createClassification(
originalMessage: string,
mapping: SecureErrorMapping,
context?: ErrorContext
_context?: ErrorContext
): ErrorClassification {
// Determine category and severity based on error code
const category = this.determineCategory(mapping.code);
@ -320,50 +365,50 @@ export class SecureErrorMapperService {
return {
category,
severity,
mapping
mapping,
};
}
private determineCategory(code: string): ErrorClassification['category'] {
if (code.startsWith('AUTH_')) return 'authentication';
if (code.startsWith('AUTHZ_')) return 'authorization';
if (code.startsWith('VAL_')) return 'validation';
if (code.startsWith('BIZ_')) return 'business';
if (code.startsWith('SYS_')) return 'system';
return 'system';
private determineCategory(code: string): ErrorClassification["category"] {
if (code.startsWith("AUTH_")) return "authentication";
if (code.startsWith("AUTHZ_")) return "authorization";
if (code.startsWith("VAL_")) return "validation";
if (code.startsWith("BIZ_")) return "business";
if (code.startsWith("SYS_")) return "system";
return "system";
}
private determineSeverity(code: string, message: string): ErrorClassification['severity'] {
private determineSeverity(code: string, message: string): ErrorClassification["severity"] {
// Critical system errors
if (code === 'SYS_001' || code === 'SYS_003') return 'critical';
if (code === "SYS_001" || code === "SYS_003") return "critical";
// High severity for authentication issues
if (code.startsWith('AUTH_') && message.toLowerCase().includes('breach')) return 'high';
if (code.startsWith("AUTH_") && message.toLowerCase().includes("breach")) return "high";
// Medium for external service issues
if (code === 'SYS_002') return 'medium';
if (code === "SYS_002") return "medium";
// Low for validation and business logic
if (code.startsWith('VAL_') || code.startsWith('BIZ_')) return 'low';
return 'medium';
if (code.startsWith("VAL_") || code.startsWith("BIZ_")) return "low";
return "medium";
}
private getDefaultMapping(message: string): SecureErrorMapping {
// Analyze message for sensitivity
if (this.containsSensitiveInfo(message)) {
return {
code: 'SYS_001',
publicMessage: 'A system error occurred. Please try again later',
logLevel: 'error',
shouldAlert: true
code: "SYS_001",
publicMessage: "A system error occurred. Please try again later",
logLevel: "error",
shouldAlert: true,
};
}
return {
code: 'GEN_001',
publicMessage: 'An unexpected error occurred',
logLevel: 'error'
code: "GEN_001",
publicMessage: "An unexpected error occurred",
logLevel: "error",
};
}
@ -374,9 +419,9 @@ export class SecureErrorMapperService {
/file|path|directory/i,
/\s+at\s+.*\.js:\d+/i, // Stack traces
/[a-zA-Z]:[\\\/]/, // Windows paths
/\/[a-zA-Z0-9._\-/]+\.(js|ts|py|php)/i, // Unix paths
/[/][a-zA-Z0-9._\-/]+\.(js|ts|py|php)/i, // Unix paths
/\b(?:\d{1,3}\.){3}\d{1,3}\b/, // IP addresses
/[A-Za-z0-9]{32,}/ // Long tokens/hashes
/[A-Za-z0-9]{32,}/, // Long tokens/hashes
];
return sensitivePatterns.some(pattern => pattern.test(message));
@ -386,22 +431,22 @@ export class SecureErrorMapperService {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
if (typeof error === "string") {
return error;
}
if (typeof error === 'object' && error !== null) {
if (typeof error === "object" && error !== null) {
const obj = error as Record<string, unknown>;
if (typeof obj.message === 'string') {
if (typeof obj.message === "string") {
return obj.message;
}
}
return 'Unknown error';
return "Unknown error";
}
private extractErrorCode(error: unknown): string | null {
if (typeof error === 'object' && error !== null) {
if (typeof error === "object" && error !== null) {
const obj = error as Record<string, unknown>;
if (typeof obj.code === 'string') {
if (typeof obj.code === "string") {
return obj.code;
}
}
@ -409,29 +454,31 @@ export class SecureErrorMapperService {
}
private sanitizeForLogging(message: string): string {
return message
// Remove file paths
.replace(/\/[a-zA-Z0-9._\-/]+\.(js|ts|py|php)/g, '[file]')
// Remove stack traces
.replace(/\s+at\s+.*/g, '')
// Remove absolute paths
.replace(/[a-zA-Z]:[\\\/][^:]+/g, '[path]')
// Remove IP addresses
.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '[ip]')
// Remove URLs with credentials
.replace(/https?:\/\/[^:]+:[^@]+@[^\s]+/g, '[url]')
// Remove potential secrets
.replace(/\b[A-Za-z0-9]{32,}\b/g, '[token]')
.trim();
return (
message
// Remove file paths
.replace(/[/][a-zA-Z0-9._\-/]+\.(js|ts|py|php)/g, "[file]")
// Remove stack traces
.replace(/\s+at\s+.*/g, "")
// Remove absolute paths
.replace(/[a-zA-Z]:[\\\/][^:]+/g, "[path]")
// Remove IP addresses
.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "[ip]")
// Remove URLs with credentials
.replace(/https?:\/\/[^:]+:[^@]+@[^\s]+/g, "[url]")
// Remove potential secrets
.replace(/\b[A-Za-z0-9]{32,}\b/g, "[token]")
.trim()
);
}
private sanitizeForDevelopment(message: string): string {
// In development, show more but still remove the most sensitive parts
return message
.replace(/password[=:]\s*[^\s]+/gi, 'password=[HIDDEN]')
.replace(/secret[=:]\s*[^\s]+/gi, 'secret=[HIDDEN]')
.replace(/token[=:]\s*[^\s]+/gi, 'token=[HIDDEN]')
.replace(/key[=:]\s*[^\s]+/gi, 'key=[HIDDEN]');
.replace(/password[=:]\s*[^\s]+/gi, "password=[HIDDEN]")
.replace(/secret[=:]\s*[^\s]+/gi, "secret=[HIDDEN]")
.replace(/token[=:]\s*[^\s]+/gi, "token=[HIDDEN]")
.replace(/key[=:]\s*[^\s]+/gi, "key=[HIDDEN]");
}
private sendSecurityAlert(
@ -441,14 +488,14 @@ export class SecureErrorMapperService {
): void {
// In a real implementation, this would send alerts to monitoring systems
// like Slack, PagerDuty, or custom alerting systems
this.logger.error('SECURITY ALERT TRIGGERED', {
alertType: 'CRITICAL_ERROR',
this.logger.error("SECURITY ALERT TRIGGERED", {
alertType: "CRITICAL_ERROR",
errorCode: classification.mapping.code,
category: classification.category,
severity: classification.severity,
context,
timestamp: new Date().toISOString(),
...logData
...logData,
});
}
}

View File

@ -192,15 +192,16 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
const errorData = data as SalesforcePubSubError;
const details = errorData.details || "";
const metadata = errorData.metadata || {};
const errorCodes = Array.isArray(metadata["error-code"])
? metadata["error-code"]
: [];
const errorCodes = Array.isArray(metadata["error-code"]) ? metadata["error-code"] : [];
const hasCorruptionCode = errorCodes.some(code =>
String(code).includes("replayid.corrupted")
);
const mentionsReplayValidation = /Replay ID validation failed/i.test(details);
if ((hasCorruptionCode || mentionsReplayValidation) && !this.replayCorruptionRecovered) {
if (
(hasCorruptionCode || mentionsReplayValidation) &&
!this.replayCorruptionRecovered
) {
this.replayCorruptionRecovered = true;
const key = sfReplayKey(this.channel);
await this.cache.del(key);
@ -291,7 +292,7 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
this.replayCorruptionRecovered = false;
}
return this.client!;
return this.client;
})();
try {
@ -326,11 +327,7 @@ export class SalesforcePubSubSubscriber implements OnModuleInit, OnModuleDestroy
Number(storedReplay)
);
} else if (replayMode === "ALL") {
await client.subscribeFromEarliestEvent(
this.channel,
this.subscribeCallback,
numRequested
);
await client.subscribeFromEarliestEvent(this.channel, this.subscribeCallback, numRequested);
} else {
await client.subscribe(this.channel, this.subscribeCallback, numRequested);
}

View File

@ -363,7 +363,7 @@ export class SalesforceConnection {
*/
private getQueryPriority(soql: string): number {
const lowerSoql = soql.toLowerCase();
// High priority queries (critical for user experience)
if (
lowerSoql.includes("account") ||
@ -372,7 +372,7 @@ export class SalesforceConnection {
) {
return 8;
}
// Medium priority queries
if (
lowerSoql.includes("order") ||
@ -381,7 +381,7 @@ export class SalesforceConnection {
) {
return 5;
}
// Low priority (bulk queries, reports)
return 2;
}
@ -391,7 +391,7 @@ export class SalesforceConnection {
*/
private isLongRunningQuery(soql: string): boolean {
const lowerSoql = soql.toLowerCase();
// Queries likely to take >20 seconds
return (
lowerSoql.includes("count(") ||

View File

@ -67,32 +67,35 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
options: WhmcsRequestOptions = {}
): Promise<T> {
// Wrap the actual request in the queue to prevent race conditions
return this.requestQueue.execute(async () => {
try {
const config = this.configService.getConfig();
const response = await this.httpClient.makeRequest<T>(config, action, params, options);
return this.requestQueue.execute(
async () => {
try {
const config = this.configService.getConfig();
const response = await this.httpClient.makeRequest<T>(config, action, params, options);
if (response.result === "error") {
const errorResponse = response as WhmcsErrorResponse;
this.errorHandler.handleApiError(errorResponse, action, params);
if (response.result === "error") {
const errorResponse = response as WhmcsErrorResponse;
this.errorHandler.handleApiError(errorResponse, action, params);
}
return response.data as T;
} catch (error) {
// If it's already a handled error, re-throw it
if (this.isHandledException(error)) {
throw error;
}
// Handle general request errors
this.errorHandler.handleRequestError(error, action, params);
}
return response.data as T;
} catch (error) {
// If it's already a handled error, re-throw it
if (this.isHandledException(error)) {
throw error;
}
// Handle general request errors
this.errorHandler.handleRequestError(error, action, params);
},
{
priority: this.getRequestPriority(action),
timeout: options.timeout,
retryAttempts: options.retryAttempts,
retryDelay: options.retryDelay,
}
}, {
priority: this.getRequestPriority(action),
timeout: options.timeout,
retryAttempts: options.retryAttempts,
retryDelay: options.retryDelay,
});
);
}
/**
@ -283,18 +286,14 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
// High priority actions (critical for user experience)
const highPriorityActions = [
"ValidateLogin",
"GetClientDetails",
"GetClientDetails",
"GetInvoice",
"CapturePayment",
"CreateSsoToken"
"CreateSsoToken",
];
// Medium priority actions (important but can wait)
const mediumPriorityActions = [
"GetInvoices",
"GetClientsProducts",
"GetPayMethods"
];
const mediumPriorityActions = ["GetInvoices", "GetClientsProducts", "GetPayMethods"];
if (highPriorityActions.includes(action)) {
return 8; // High priority

View File

@ -85,7 +85,7 @@ export class WhmcsInvoiceService {
this.logger.log(
`Fetched ${result.invoices.length} invoices for client ${clientId}, page ${page}`
);
return result as InvoiceList;
return result;
} catch (error) {
this.logger.error(`Failed to fetch invoices for client ${clientId}`, {
error: getErrorMessage(error),
@ -136,7 +136,7 @@ export class WhmcsInvoiceService {
);
const result: InvoiceList = {
invoices: invoicesWithItems as Invoice[],
invoices: invoicesWithItems,
pagination: invoiceList.pagination,
};
@ -230,7 +230,7 @@ export class WhmcsInvoiceService {
try {
const transformed = this.invoiceTransformer.transformInvoice(whmcsInvoice);
const parsed = invoiceSchema.parse(transformed);
invoices.push(parsed as Invoice);
invoices.push(parsed);
} catch (error) {
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, {
error: getErrorMessage(error),

View File

@ -1,4 +1,14 @@
import { Controller, Post, Body, UseGuards, Get, Req, HttpCode, UsePipes, Res } from "@nestjs/common";
import {
Controller,
Post,
Body,
UseGuards,
Get,
Req,
HttpCode,
UsePipes,
Res,
} from "@nestjs/common";
import type { Request, Response } from "express";
import { Throttle } from "@nestjs/throttler";
import { AuthService } from "./auth.service";
@ -99,10 +109,7 @@ export class AuthController {
@ApiResponse({ status: 409, description: "Customer already has account" })
@ApiResponse({ status: 400, description: "Customer number not found" })
@ApiResponse({ status: 429, description: "Too many validation attempts" })
async validateSignup(
@Body() validateData: ValidateSignupRequestInput,
@Req() req: Request
) {
async validateSignup(@Body() validateData: ValidateSignupRequestInput, @Req() req: Request) {
return this.authService.validateSignup(validateData, req);
}

View File

@ -40,7 +40,9 @@ export class GlobalAuthGuard extends AuthGuard("jwt") implements CanActivate {
override async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context
.switchToHttp()
.getRequest<RequestWithCookies & { method: string; url: string; route?: { path?: string } }>();
.getRequest<
RequestWithCookies & { method: string; url: string; route?: { path?: string } }
>();
const route = `${request.method} ${request.route?.path ?? request.url}`;
// Check if the route is marked as public

View File

@ -110,7 +110,10 @@ function coerceNumber(value: unknown): number | undefined {
return undefined;
}
function baseProduct(product: SalesforceCatalogProductRecord, fieldMap: SalesforceFieldMap): CatalogProductBase {
function baseProduct(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap
): CatalogProductBase {
const sku = getStringField(product, "sku", fieldMap) ?? "";
const base: CatalogProductBase = {
id: product.Id,
@ -139,12 +142,18 @@ function getBoolean(
return typeof value === "boolean" ? value : undefined;
}
function resolveBundledAddonId(product: SalesforceCatalogProductRecord, fieldMap: SalesforceFieldMap): string | undefined {
function resolveBundledAddonId(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap
): string | undefined {
const raw = getProductField(product, "bundledAddon", fieldMap);
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
}
function resolveBundledAddon(product: SalesforceCatalogProductRecord, fieldMap: SalesforceFieldMap) {
function resolveBundledAddon(
product: SalesforceCatalogProductRecord,
fieldMap: SalesforceFieldMap
) {
return {
bundledAddonId: resolveBundledAddonId(product, fieldMap),
isBundledAddon: Boolean(getBoolean(product, "isBundledAddon", fieldMap)),

View File

@ -109,7 +109,11 @@ export class OrderBuilder {
assignIfString(orderFields, orderField("accessMode", fieldMap), config.accessMode);
}
private addSimFields(orderFields: Record<string, unknown>, body: OrderBusinessValidation, fieldMap: SalesforceFieldMap): void {
private addSimFields(
orderFields: Record<string, unknown>,
body: OrderBusinessValidation,
fieldMap: SalesforceFieldMap
): void {
const config = body.configurations || {};
assignIfString(orderFields, orderField("simType", fieldMap), config.simType);
assignIfString(orderFields, orderField("eid", fieldMap), config.eid);
@ -119,7 +123,11 @@ export class OrderBuilder {
assignIfString(orderFields, mnpField("reservationNumber", fieldMap), config.mnpNumber);
assignIfString(orderFields, mnpField("expiryDate", fieldMap), config.mnpExpiry);
assignIfString(orderFields, mnpField("phoneNumber", fieldMap), config.mnpPhone);
assignIfString(orderFields, mnpField("mvnoAccountNumber", fieldMap), config.mvnoAccountNumber);
assignIfString(
orderFields,
mnpField("mvnoAccountNumber", fieldMap),
config.mvnoAccountNumber
);
assignIfString(orderFields, mnpField("portingLastName", fieldMap), config.portingLastName);
assignIfString(orderFields, mnpField("portingFirstName", fieldMap), config.portingFirstName);
assignIfString(
@ -133,7 +141,11 @@ export class OrderBuilder {
config.portingFirstNameKatakana
);
assignIfString(orderFields, mnpField("portingGender", fieldMap), config.portingGender);
assignIfString(orderFields, mnpField("portingDateOfBirth", fieldMap), config.portingDateOfBirth);
assignIfString(
orderFields,
mnpField("portingDateOfBirth", fieldMap),
config.portingDateOfBirth
);
}
}

View File

@ -103,7 +103,7 @@ export class OrderFulfillmentOrchestrator {
} catch (error) {
this.logger.error("Fulfillment validation failed", {
sfOrderId,
error: getErrorMessage(error)
error: getErrorMessage(error),
});
throw error;
}
@ -118,164 +118,169 @@ export class OrderFulfillmentOrchestrator {
} catch (error) {
this.logger.error("Failed to get order details", {
sfOrderId,
error: getErrorMessage(error)
error: getErrorMessage(error),
});
throw error;
}
// Step 3: Execute the main fulfillment workflow as a distributed transaction
const fulfillmentResult = await this.distributedTransactionService.executeDistributedTransaction([
{
id: 'sf_status_update',
description: 'Update Salesforce order status to Activating',
execute: async () => {
const fields = this.fieldMapService.getFieldMap();
return await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Activating",
});
},
rollback: async () => {
const fields = this.fieldMapService.getFieldMap();
await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Failed",
});
},
critical: true
},
{
id: 'mapping',
description: 'Map OrderItems to WHMCS format',
execute: async () => {
if (!context.orderDetails) {
throw new Error("Order details are required for mapping");
}
return this.orderWhmcsMapper.mapOrderItemsToWhmcs(
context.orderDetails.items
);
},
critical: true
},
{
id: 'whmcs_create',
description: 'Create order in WHMCS',
execute: async () => {
const mappingResult = fulfillmentResult.stepResults?.mapping;
if (!mappingResult) {
throw new Error("Mapping result is not available");
}
const fulfillmentResult =
await this.distributedTransactionService.executeDistributedTransaction(
[
{
id: "sf_status_update",
description: "Update Salesforce order status to Activating",
execute: async () => {
const fields = this.fieldMapService.getFieldMap();
return await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Activating",
});
},
rollback: async () => {
const fields = this.fieldMapService.getFieldMap();
await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Failed",
});
},
critical: true,
},
{
id: "mapping",
description: "Map OrderItems to WHMCS format",
execute: async () => {
if (!context.orderDetails) {
throw new Error("Order details are required for mapping");
}
return this.orderWhmcsMapper.mapOrderItemsToWhmcs(context.orderDetails.items);
},
critical: true,
},
{
id: "whmcs_create",
description: "Create order in WHMCS",
execute: async () => {
const mappingResult = fulfillmentResult.stepResults?.mapping;
if (!mappingResult) {
throw new Error("Mapping result is not available");
}
const orderNotes = this.orderWhmcsMapper.createOrderNotes(
sfOrderId,
`Provisioned from Salesforce Order ${sfOrderId}`
);
const orderNotes = this.orderWhmcsMapper.createOrderNotes(
sfOrderId,
`Provisioned from Salesforce Order ${sfOrderId}`
);
return await this.whmcsOrderService.addOrder({
clientId: context.validation!.clientId,
items: mappingResult.whmcsItems,
paymentMethod: "stripe",
promoCode: "1st Month Free (Monthly Plan)",
sfOrderId,
notes: orderNotes,
noinvoiceemail: true,
noemail: true,
});
},
rollback: async () => {
const createResult = fulfillmentResult.stepResults?.whmcs_create;
if (createResult?.orderId) {
// Note: WHMCS doesn't have an automated cancel API
// Manual intervention required for order cleanup
this.logger.error("WHMCS order created but fulfillment failed - manual cleanup required", {
orderId: createResult.orderId,
sfOrderId,
action: "MANUAL_CLEANUP_REQUIRED"
});
}
},
critical: true
},
{
id: 'whmcs_accept',
description: 'Accept/provision order in WHMCS',
execute: async () => {
const createResult = fulfillmentResult.stepResults?.whmcs_create;
if (!createResult?.orderId) {
throw new Error("WHMCS order ID missing before acceptance step");
}
return await this.whmcsOrderService.addOrder({
clientId: context.validation!.clientId,
items: mappingResult.whmcsItems,
paymentMethod: "stripe",
promoCode: "1st Month Free (Monthly Plan)",
sfOrderId,
notes: orderNotes,
noinvoiceemail: true,
noemail: true,
});
},
rollback: async () => {
const createResult = fulfillmentResult.stepResults?.whmcs_create;
if (createResult?.orderId) {
// Note: WHMCS doesn't have an automated cancel API
// Manual intervention required for order cleanup
this.logger.error(
"WHMCS order created but fulfillment failed - manual cleanup required",
{
orderId: createResult.orderId,
sfOrderId,
action: "MANUAL_CLEANUP_REQUIRED",
}
);
}
},
critical: true,
},
{
id: "whmcs_accept",
description: "Accept/provision order in WHMCS",
execute: async () => {
const createResult = fulfillmentResult.stepResults?.whmcs_create;
if (!createResult?.orderId) {
throw new Error("WHMCS order ID missing before acceptance step");
}
return await this.whmcsOrderService.acceptOrder(
createResult.orderId,
sfOrderId
);
},
rollback: async () => {
const acceptResult = fulfillmentResult.stepResults?.whmcs_accept;
if (acceptResult?.orderId) {
// Note: WHMCS doesn't have an automated cancel API for accepted orders
// Manual intervention required for service termination
this.logger.error("WHMCS order accepted but fulfillment failed - manual cleanup required", {
orderId: acceptResult.orderId,
serviceIds: acceptResult.serviceIds,
sfOrderId,
action: "MANUAL_SERVICE_TERMINATION_REQUIRED"
});
}
},
critical: true
},
{
id: 'sim_fulfillment',
description: 'SIM-specific fulfillment (if applicable)',
execute: async () => {
if (context.orderDetails?.orderType === "SIM") {
const configurations = this.extractConfigurations(payload.configurations);
await this.simFulfillmentService.fulfillSimOrder({
orderDetails: context.orderDetails,
configurations,
});
return { completed: true };
}
return { skipped: true };
},
critical: false // SIM fulfillment failure shouldn't rollback the entire order
},
{
id: 'sf_success_update',
description: 'Update Salesforce with success',
execute: async () => {
const fields = this.fieldMapService.getFieldMap();
const whmcsResult = fulfillmentResult.stepResults?.whmcs_accept;
return await this.salesforceService.updateOrder({
Id: sfOrderId,
Status: "Completed",
[fields.order.activationStatus]: "Activated",
[fields.order.whmcsOrderId]: whmcsResult?.orderId?.toString(),
});
},
rollback: async () => {
const fields = this.fieldMapService.getFieldMap();
await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Failed",
});
},
critical: true
}
], {
description: `Order fulfillment for ${sfOrderId}`,
timeout: 300000, // 5 minutes
continueOnNonCriticalFailure: true
});
return await this.whmcsOrderService.acceptOrder(createResult.orderId, sfOrderId);
},
rollback: async () => {
const acceptResult = fulfillmentResult.stepResults?.whmcs_accept;
if (acceptResult?.orderId) {
// Note: WHMCS doesn't have an automated cancel API for accepted orders
// Manual intervention required for service termination
this.logger.error(
"WHMCS order accepted but fulfillment failed - manual cleanup required",
{
orderId: acceptResult.orderId,
serviceIds: acceptResult.serviceIds,
sfOrderId,
action: "MANUAL_SERVICE_TERMINATION_REQUIRED",
}
);
}
},
critical: true,
},
{
id: "sim_fulfillment",
description: "SIM-specific fulfillment (if applicable)",
execute: async () => {
if (context.orderDetails?.orderType === "SIM") {
const configurations = this.extractConfigurations(payload.configurations);
await this.simFulfillmentService.fulfillSimOrder({
orderDetails: context.orderDetails,
configurations,
});
return { completed: true };
}
return { skipped: true };
},
critical: false, // SIM fulfillment failure shouldn't rollback the entire order
},
{
id: "sf_success_update",
description: "Update Salesforce with success",
execute: async () => {
const fields = this.fieldMapService.getFieldMap();
const whmcsResult = fulfillmentResult.stepResults?.whmcs_accept;
return await this.salesforceService.updateOrder({
Id: sfOrderId,
Status: "Completed",
[fields.order.activationStatus]: "Activated",
[fields.order.whmcsOrderId]: whmcsResult?.orderId?.toString(),
});
},
rollback: async () => {
const fields = this.fieldMapService.getFieldMap();
await this.salesforceService.updateOrder({
Id: sfOrderId,
[fields.order.activationStatus]: "Failed",
});
},
critical: true,
},
],
{
description: `Order fulfillment for ${sfOrderId}`,
timeout: 300000, // 5 minutes
continueOnNonCriticalFailure: true,
}
);
if (!fulfillmentResult.success) {
this.logger.error("Fulfillment transaction failed", {
sfOrderId,
error: fulfillmentResult.error,
stepsExecuted: fulfillmentResult.stepsExecuted,
stepsRolledBack: fulfillmentResult.stepsRolledBack
stepsRolledBack: fulfillmentResult.stepsRolledBack,
});
throw new Error(fulfillmentResult.error || "Fulfillment transaction failed");
}
@ -287,7 +292,7 @@ export class OrderFulfillmentOrchestrator {
this.logger.log("Transactional fulfillment completed successfully", {
sfOrderId,
stepsExecuted: fulfillmentResult.stepsExecuted,
duration: fulfillmentResult.duration
duration: fulfillmentResult.duration,
});
return context;

View File

@ -116,7 +116,9 @@ export class OrderPricebookService {
internetOfferingType: product
? getStringField(product, "internetOfferingType", fields)
: undefined,
internetPlanTier: product ? getStringField(product, "internetPlanTier", fields) : undefined,
internetPlanTier: product
? getStringField(product, "internetPlanTier", fields)
: undefined,
vpnRegion: product ? getStringField(product, "vpnRegion", fields) : undefined,
});
}

0
customer-portal@1.0.0 Normal file
View File

35
docs/BUNDLE_ANALYSIS.md Normal file
View File

@ -0,0 +1,35 @@
# 📊 Bundle Analysis
Simple bundle size analysis for the customer portal.
## 🎯 Quick Commands
```bash
# Analyze frontend bundle
pnpm analyze
# Run bundle analysis script
pnpm bundle-analyze
```
## 📈 What to Look For
- **First Load JS**: < 250KB (good)
- **Total Bundle**: < 1MB (good)
- **Large Dependencies**: Consider alternatives
## 🔧 Simple Optimizations
1. **Dynamic Imports**: Use `lazy()` for heavy components
2. **Image Optimization**: Use Next.js `Image` component
3. **Tree Shaking**: Import only what you need
```typescript
// Good: Specific imports
import { debounce } from 'lodash-es';
// Bad: Full library import
import * as _ from 'lodash';
```
That's it! Keep it simple.

View File

@ -1,383 +0,0 @@
# 📊 Bundle Analysis Guide
Simple guide for analyzing and optimizing bundle sizes.
## 🎯 Quick Analysis
### Frontend Bundle Analysis
```bash
# Analyze bundle size
pnpm analyze
# Or use the script
pnpm bundle-analyze
```
### Key Metrics to Monitor
- **First Load JS**: Should be < 250KB
- **Total Bundle Size**: Should be < 1MB
- **Largest Chunks**: Identify optimization targets
## 🎯 Frontend Optimizations
### 1. Bundle Analysis & Code Splitting
```bash
# Analyze current bundle size
cd apps/portal
pnpm run analyze
# Build with analysis
pnpm run build:analyze
```
### 2. Dynamic Imports
```typescript
// Before: Static import
import { HeavyComponent } from './HeavyComponent';
// After: Dynamic import
const HeavyComponent = lazy(() => import('./HeavyComponent'));
// Route-level code splitting
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Orders = lazy(() => import('./pages/Orders'));
```
### 3. Image Optimization
```typescript
// Use Next.js Image component with optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={800}
height={600}
priority={false} // Lazy load non-critical images
placeholder="blur" // Add blur placeholder
/>
```
### 4. Tree Shaking Optimization
```typescript
// Before: Import entire library
import * as _ from 'lodash';
// After: Import specific functions
import { debounce, throttle } from 'lodash-es';
// Or use individual packages
import debounce from 'lodash.debounce';
```
### 5. React Query Optimization
```typescript
// Optimize React Query cache
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Reduce memory usage
cacheTime: 5 * 60 * 1000, // 5 minutes
staleTime: 1 * 60 * 1000, // 1 minute
// Limit concurrent queries
refetchOnWindowFocus: false,
},
},
});
```
## 🎯 Backend Optimizations
### 1. Heap Size Optimization
```json
// package.json - Optimized heap sizes
{
"scripts": {
"dev": "NODE_OPTIONS=\"--max-old-space-size=2048\" nest start --watch",
"build": "NODE_OPTIONS=\"--max-old-space-size=3072\" nest build",
"type-check": "NODE_OPTIONS=\"--max-old-space-size=4096\" tsc --noEmit"
}
}
```
### 2. Streaming Responses
```typescript
// For large data responses
@Get('large-dataset')
async getLargeDataset(@Res() res: Response) {
const stream = this.dataService.createDataStream();
res.setHeader('Content-Type', 'application/json');
res.setHeader('Transfer-Encoding', 'chunked');
stream.pipe(res);
}
```
### 3. Memory-Efficient Pagination
```typescript
// Cursor-based pagination instead of offset
interface PaginationOptions {
cursor?: string;
limit: number; // Max 100
}
async findWithCursor(options: PaginationOptions) {
return this.prisma.order.findMany({
take: Math.min(options.limit, 100),
...(options.cursor && {
cursor: { id: options.cursor },
skip: 1,
}),
orderBy: { createdAt: 'desc' },
});
}
```
### 4. Request/Response Caching
```typescript
// Memory-efficient caching
@Injectable()
export class CacheService {
private readonly cache = new Map<string, { data: any; expires: number }>();
private readonly maxSize = 1000; // Limit cache size
set(key: string, data: any, ttl: number = 300000) {
// Implement LRU eviction
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
data,
expires: Date.now() + ttl,
});
}
}
```
### 5. Database Connection Optimization
```typescript
// Optimize Prisma connection pool
const prisma = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
// Optimize connection pool
__internal: {
engine: {
connectionLimit: 10, // Reduce from default 20
},
},
});
```
## 🎯 Dependency Optimizations
### 1. Replace Heavy Dependencies
```bash
# Before: moment.js (67KB)
npm uninstall moment
# After: date-fns (13KB with tree shaking)
npm install date-fns
# Before: lodash (71KB)
npm uninstall lodash
# After: Individual functions or native alternatives
npm install lodash-es # Better tree shaking
```
### 2. Bundle Analysis Results
```bash
# Run bundle analysis
./scripts/memory-optimization.sh
# Key metrics to monitor:
# - First Load JS: < 250KB
# - Total Bundle Size: < 1MB
# - Largest Chunks: Identify optimization targets
```
### 3. Webpack Optimizations (Already Implemented)
- **Code Splitting**: Separate vendor, common, and UI chunks
- **Tree Shaking**: Remove unused code
- **Compression**: Gzip/Brotli compression
- **Caching**: Long-term caching for static assets
## 🎯 Runtime Optimizations
### 1. Memory Leak Detection
```typescript
// Add memory monitoring
@Injectable()
export class MemoryMonitorService {
@Cron('*/5 * * * *') // Every 5 minutes
checkMemoryUsage() {
const usage = process.memoryUsage();
if (usage.heapUsed > 500 * 1024 * 1024) { // 500MB
this.logger.warn('High memory usage detected', {
heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)}MB`,
heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)}MB`,
external: `${Math.round(usage.external / 1024 / 1024)}MB`,
});
}
}
}
```
### 2. Garbage Collection Optimization
```bash
# Enable GC logging in production
NODE_OPTIONS="--max-old-space-size=2048 --gc-interval=100" npm start
# Monitor GC patterns
NODE_OPTIONS="--trace-gc --trace-gc-verbose" npm run dev
```
### 3. Worker Threads for CPU-Intensive Tasks
```typescript
// For heavy computations
import { Worker, isMainThread, parentPort } from 'worker_threads';
if (isMainThread) {
// Main thread
const worker = new Worker(__filename);
worker.postMessage({ data: largeDataset });
worker.on('message', (result) => {
// Handle processed result
});
} else {
// Worker thread
parentPort?.on('message', ({ data }) => {
const result = processLargeDataset(data);
parentPort?.postMessage(result);
});
}
```
## 📈 Monitoring & Metrics
### 1. Performance Monitoring
```typescript
// Add performance metrics
@Injectable()
export class PerformanceService {
trackMemoryUsage(operation: string) {
const start = process.memoryUsage();
return {
end: () => {
const end = process.memoryUsage();
const diff = {
heapUsed: end.heapUsed - start.heapUsed,
heapTotal: end.heapTotal - start.heapTotal,
};
this.logger.debug(`Memory usage for ${operation}`, diff);
},
};
}
}
```
### 2. Bundle Size Monitoring
```json
// Add to CI/CD pipeline
{
"scripts": {
"build:check-size": "npm run build && bundlesize"
},
"bundlesize": [
{
"path": ".next/static/js/*.js",
"maxSize": "250kb"
},
{
"path": ".next/static/css/*.css",
"maxSize": "50kb"
}
]
}
```
## 🚀 Implementation Checklist
### Immediate Actions (Week 1)
- [ ] Run bundle analysis: `pnpm run analyze`
- [ ] Implement dynamic imports for heavy components
- [ ] Optimize image loading with Next.js Image
- [ ] Reduce heap allocation in development
### Short-term (Week 2-3)
- [ ] Replace heavy dependencies (moment → date-fns)
- [ ] Implement request caching
- [ ] Add memory monitoring
- [ ] Optimize database connection pool
### Long-term (Month 1)
- [ ] Implement streaming for large responses
- [ ] Add worker threads for CPU-intensive tasks
- [ ] Set up continuous bundle size monitoring
- [ ] Implement advanced caching strategies
## 🎯 Expected Results
### Memory Reduction Targets
- **Frontend Bundle**: 30-50% reduction
- **Backend Heap**: 25-40% reduction
- **Build Time**: 20-30% improvement
- **Runtime Memory**: 35-50% reduction
### Performance Improvements
- **First Load**: < 2 seconds
- **Page Transitions**: < 500ms
- **API Response**: < 200ms (95th percentile)
- **Memory Stability**: No memory leaks in 24h+ runs
## 🔧 Tools & Commands
```bash
# Frontend analysis
cd apps/portal && pnpm run analyze
# Backend memory check
cd apps/bff && NODE_OPTIONS="--trace-gc" pnpm dev
# Full optimization analysis
./scripts/memory-optimization.sh
# Dependency audit
pnpm audit --recursive
# Bundle size check
pnpm run build && ls -la .next/static/js/
```
---
**Note**: Always test memory optimizations in a staging environment before deploying to production. Monitor application performance and user experience after implementing changes.

0
nest Normal file
View File

View File

@ -79,10 +79,7 @@ export const checkPasswordNeededRequestSchema = z.object({
});
export const refreshTokenRequestSchema = z.object({
refreshToken: z
.string()
.min(1, "Refresh token is required")
.optional(),
refreshToken: z.string().min(1, "Refresh token is required").optional(),
deviceId: z.string().optional(),
});

View File

@ -7,5 +7,4 @@
export { z } from "zod";
// Framework-specific exports
export * from "./nestjs";
export * from "./react";

View File

@ -0,0 +1,6 @@
/**
* NestJS-specific validation utilities
* Import this directly in backend code: import { ... } from "@customer-portal/validation/nestjs"
*/
export * from "./nestjs/index";

146
pnpm-lock.yaml generated
View File

@ -301,6 +301,9 @@ importers:
specifier: ^5.0.8
version: 5.0.8(@types/react@19.1.12)(react@19.1.1)
devDependencies:
'@next/bundle-analyzer':
specifier: ^15.5.0
version: 15.5.4
'@tailwindcss/postcss':
specifier: ^4.1.12
version: 4.1.13
@ -319,6 +322,9 @@ importers:
typescript:
specifier: ^5.9.2
version: 5.9.2
webpack-bundle-analyzer:
specifier: ^4.10.2
version: 4.10.2
packages/domain:
dependencies:
@ -576,6 +582,10 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
'@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==}
@ -1384,6 +1394,9 @@ packages:
'@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
reflect-metadata: ^0.1.13 || ^0.2.0
'@next/bundle-analyzer@15.5.4':
resolution: {integrity: sha512-wMtpIjEHi+B/wC34ZbEcacGIPgQTwTFjjp0+F742s9TxC6QwT0MwB/O0QEgalMe8s3SH/K09DO0gmTvUSJrLRA==}
'@next/env@15.5.0':
resolution: {integrity: sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==}
@ -1474,6 +1487,9 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@prisma/client@6.16.0':
resolution: {integrity: sha512-FYkFJtgwpwJRMxtmrB26y7gtpR372kyChw6lWng5TMmvn5V+uisy0OyllO5EJD1s8lX78V8X3XjhiXOoMLnu3w==}
engines: {node: '>=18.18'}
@ -2476,6 +2492,10 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
comment-json@4.2.5:
resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==}
engines: {node: '>= 6'}
@ -2593,6 +2613,9 @@ packages:
dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@ -2693,6 +2716,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -3212,6 +3238,10 @@ packages:
graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
gzip-size@6.0.0:
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
engines: {node: '>=10'}
handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'}
@ -3423,6 +3453,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
is-plain-object@5.0.0:
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
engines: {node: '>=0.10.0'}
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
@ -4000,6 +4034,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -4194,6 +4232,10 @@ packages:
openapi-typescript-helpers@0.0.15:
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
opener@1.5.2:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
hasBin: true
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@ -4676,6 +4718,10 @@ packages:
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
sirv@2.0.4:
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
engines: {node: '>= 10'}
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@ -4926,6 +4972,10 @@ packages:
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
engines: {node: '>=14.16'}
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tough-cookie@6.0.0:
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
engines: {node: '>=16'}
@ -5159,6 +5209,16 @@ packages:
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webpack-bundle-analyzer@4.10.1:
resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==}
engines: {node: '>= 10.13.0'}
hasBin: true
webpack-bundle-analyzer@4.10.2:
resolution: {integrity: sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==}
engines: {node: '>= 10.13.0'}
hasBin: true
webpack-node-externals@3.0.0:
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
engines: {node: '>=6'}
@ -5238,6 +5298,18 @@ packages:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
ws@7.5.10:
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
engines: {node: '>=8.3.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
@ -5543,6 +5615,8 @@ snapshots:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@discoveryjs/json-ext@0.5.7': {}
'@emnapi/core@1.5.0':
dependencies:
'@emnapi/wasi-threads': 1.1.0
@ -6367,6 +6441,13 @@ snapshots:
'@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)
reflect-metadata: 0.2.2
'@next/bundle-analyzer@15.5.4':
dependencies:
webpack-bundle-analyzer: 4.10.1
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@next/env@15.5.0': {}
'@next/eslint-plugin-next@15.5.0':
@ -6426,6 +6507,8 @@ snapshots:
'@pkgr/core@0.2.9': {}
'@polka/url@1.0.0-next.29': {}
'@prisma/client@6.16.0(prisma@6.16.0(typescript@5.9.2))(typescript@5.9.2)':
optionalDependencies:
prisma: 6.16.0(typescript@5.9.2)
@ -7521,6 +7604,8 @@ snapshots:
commander@4.1.1: {}
commander@7.2.0: {}
comment-json@4.2.5:
dependencies:
array-timsort: 1.0.3
@ -7631,6 +7716,8 @@ snapshots:
dateformat@4.6.3: {}
debounce@1.2.1: {}
debug@3.2.7:
dependencies:
ms: 2.1.3
@ -7702,6 +7789,8 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
duplexer@0.1.2: {}
eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
@ -8441,6 +8530,10 @@ snapshots:
graphemer@1.4.0: {}
gzip-size@6.0.0:
dependencies:
duplexer: 0.1.2
handlebars@4.7.8:
dependencies:
minimist: 1.2.8
@ -8660,6 +8753,8 @@ snapshots:
is-number@7.0.0: {}
is-plain-object@5.0.0: {}
is-promise@4.0.0: {}
is-regex@1.2.1:
@ -9390,6 +9485,8 @@ snapshots:
mkdirp@3.0.1: {}
mrmime@2.0.1: {}
ms@2.1.3: {}
msgpackr-extract@3.0.3:
@ -9584,6 +9681,8 @@ snapshots:
openapi-typescript-helpers@0.0.15: {}
opener@1.5.2: {}
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@ -10163,6 +10262,12 @@ snapshots:
dependencies:
is-arrayish: 0.3.2
sirv@2.0.4:
dependencies:
'@polka/url': 1.0.0-next.29
mrmime: 2.0.1
totalist: 3.0.1
slash@3.0.0: {}
sonic-boom@4.2.0:
@ -10421,6 +10526,8 @@ snapshots:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
totalist@3.0.1: {}
tough-cookie@6.0.0:
dependencies:
tldts: 7.0.13
@ -10672,6 +10779,43 @@ snapshots:
webidl-conversions@3.0.1: {}
webpack-bundle-analyzer@4.10.1:
dependencies:
'@discoveryjs/json-ext': 0.5.7
acorn: 8.15.0
acorn-walk: 8.3.4
commander: 7.2.0
debounce: 1.2.1
escape-string-regexp: 4.0.0
gzip-size: 6.0.0
html-escaper: 2.0.2
is-plain-object: 5.0.0
opener: 1.5.2
picocolors: 1.1.1
sirv: 2.0.4
ws: 7.5.10
transitivePeerDependencies:
- bufferutil
- utf-8-validate
webpack-bundle-analyzer@4.10.2:
dependencies:
'@discoveryjs/json-ext': 0.5.7
acorn: 8.15.0
acorn-walk: 8.3.4
commander: 7.2.0
debounce: 1.2.1
escape-string-regexp: 4.0.0
gzip-size: 6.0.0
html-escaper: 2.0.2
opener: 1.5.2
picocolors: 1.1.1
sirv: 2.0.4
ws: 7.5.10
transitivePeerDependencies:
- bufferutil
- utf-8-validate
webpack-node-externals@3.0.0: {}
webpack-sources@3.3.3: {}
@ -10797,6 +10941,8 @@ snapshots:
imurmurhash: 0.1.4
signal-exit: 4.1.0
ws@7.5.10: {}
xml2js@0.6.2:
dependencies:
sax: 1.4.1

0
rm Normal file
View File

View File

@ -328,9 +328,21 @@ start_apps() {
log "🔨 Building shared package..."
pnpm --filter @customer-portal/domain build
# Build BFF before watch (ensures dist exists)
log "🔨 Building BFF for initial setup..."
(cd "$PROJECT_ROOT/apps/bff" && pnpm tsc -p tsconfig.build.json)
# Build BFF before watch (ensures dist exists). Use Nest build for correct emit.
log "🔨 Building BFF for initial setup (ts emit)..."
(
cd "$PROJECT_ROOT/apps/bff" \
&& pnpm clean \
&& rm -f tsconfig.build.tsbuildinfo \
&& pnpm build || pnpm exec tsc -b --force tsconfig.build.json
)
if [ ! -d "$PROJECT_ROOT/apps/bff/dist" ]; then
warn "BFF dist not found after build; forcing TypeScript emit..."
(cd "$PROJECT_ROOT/apps/bff" && pnpm exec tsc -b --force tsconfig.build.json)
fi
if [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.js" ] && [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.cjs" ]; then
warn "BFF main output not found; will rely on watch to produce it."
fi
local next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"

0
tsc Normal file
View File