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", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"tsConfigPath": "tsconfig.build.json", "tsConfigPath": "tsconfig.build.json",
"deleteOutDir": true, "deleteOutDir": false,
"watchAssets": true, "watchAssets": true,
"assets": ["**/*.prisma"] "assets": ["**/*.prisma"]
} }

View File

@ -9,7 +9,8 @@
"build": "nest build", "build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start", "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:debug": "nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"lint": "eslint .", "lint": "eslint .",
@ -21,7 +22,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json", "test:e2e": "jest --config ./test/jest-e2e.json",
"type-check": "tsc --project tsconfig.json --noEmit", "type-check": "tsc --project tsconfig.json --noEmit",
"type-check:watch": "tsc --project tsconfig.json --noEmit --watch", "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:migrate": "prisma migrate dev",
"db:generate": "prisma generate", "db:generate": "prisma generate",
"db:studio": "prisma studio", "db:studio": "prisma studio",

View File

@ -91,7 +91,7 @@ export class DistributedTransactionService {
description, description,
timeout = 120000, // 2 minutes default for distributed operations timeout = 120000, // 2 minutes default for distributed operations
maxRetries = 1, // Less retries for distributed operations maxRetries = 1, // Less retries for distributed operations
continueOnNonCriticalFailure = false continueOnNonCriticalFailure = false,
} = options; } = options;
const transactionId = this.generateTransactionId(); const transactionId = this.generateTransactionId();
@ -100,7 +100,7 @@ export class DistributedTransactionService {
this.logger.log(`Starting distributed transaction [${transactionId}]`, { this.logger.log(`Starting distributed transaction [${transactionId}]`, {
description, description,
stepsCount: steps.length, stepsCount: steps.length,
timeout timeout,
}); });
const stepResults: Record<string, any> = {}; const stepResults: Record<string, any> = {};
@ -113,7 +113,7 @@ export class DistributedTransactionService {
for (const step of steps) { for (const step of steps) {
this.logger.debug(`Executing step: ${step.id} [${transactionId}]`, { this.logger.debug(`Executing step: ${step.id} [${transactionId}]`, {
description: step.description, description: step.description,
critical: step.critical critical: step.critical,
}); });
try { try {
@ -125,9 +125,8 @@ export class DistributedTransactionService {
executedSteps.push(step.id); executedSteps.push(step.id);
this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, { this.logger.debug(`Step completed: ${step.id} [${transactionId}]`, {
duration: stepDuration duration: stepDuration,
}); });
} catch (stepError) { } catch (stepError) {
lastError = stepError as Error; lastError = stepError as Error;
failedSteps.push(step.id); failedSteps.push(step.id);
@ -135,7 +134,7 @@ export class DistributedTransactionService {
this.logger.error(`Step failed: ${step.id} [${transactionId}]`, { this.logger.error(`Step failed: ${step.id} [${transactionId}]`, {
error: getErrorMessage(stepError), error: getErrorMessage(stepError),
critical: step.critical, critical: step.critical,
retryable: step.retryable retryable: step.retryable,
}); });
// If it's a critical step, stop the entire transaction // If it's a critical step, stop the entire transaction
@ -149,7 +148,9 @@ export class DistributedTransactionService {
} }
// Otherwise, log and continue // 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}]`
);
} }
} }
@ -159,7 +160,7 @@ export class DistributedTransactionService {
description, description,
duration, duration,
stepsExecuted: executedSteps.length, stepsExecuted: executedSteps.length,
failedSteps: failedSteps.length failedSteps: failedSteps.length,
}); });
return { return {
@ -169,9 +170,8 @@ export class DistributedTransactionService {
stepsExecuted: executedSteps.length, stepsExecuted: executedSteps.length,
stepsRolledBack: 0, stepsRolledBack: 0,
stepResults, stepResults,
failedSteps failedSteps,
}; };
} catch (error) { } catch (error) {
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
@ -180,7 +180,7 @@ export class DistributedTransactionService {
error: getErrorMessage(error), error: getErrorMessage(error),
duration, duration,
stepsExecuted: executedSteps.length, stepsExecuted: executedSteps.length,
failedSteps: failedSteps.length failedSteps: failedSteps.length,
}); });
// Execute rollbacks for completed steps // Execute rollbacks for completed steps
@ -198,7 +198,7 @@ export class DistributedTransactionService {
stepsExecuted: executedSteps.length, stepsExecuted: executedSteps.length,
stepsRolledBack: rollbacksExecuted, stepsRolledBack: rollbacksExecuted,
stepResults, stepResults,
failedSteps failedSteps,
}; };
} }
} }
@ -226,7 +226,7 @@ export class DistributedTransactionService {
this.logger.log(`Starting hybrid transaction [${transactionId}]`, { this.logger.log(`Starting hybrid transaction [${transactionId}]`, {
description: options.description, description: options.description,
databaseFirst, databaseFirst,
externalStepsCount: externalSteps.length externalStepsCount: externalSteps.length,
}); });
try { try {
@ -240,12 +240,12 @@ export class DistributedTransactionService {
databaseOperation, databaseOperation,
{ {
description: `${options.description} - Database Operations`, description: `${options.description} - Database Operations`,
timeout: options.timeout timeout: options.timeout,
} }
); );
if (!dbTransactionResult.success) { if (!dbTransactionResult.success) {
throw new Error(dbTransactionResult.error || 'Database transaction failed'); throw new Error(dbTransactionResult.error || "Database transaction failed");
} }
databaseResult = dbTransactionResult.data!; databaseResult = dbTransactionResult.data!;
@ -254,27 +254,29 @@ export class DistributedTransactionService {
this.logger.debug(`Executing external operations [${transactionId}]`); this.logger.debug(`Executing external operations [${transactionId}]`);
externalResult = await this.executeDistributedTransaction(externalSteps, { externalResult = await this.executeDistributedTransaction(externalSteps, {
...distributedOptions, ...distributedOptions,
description: distributedOptions.description || 'External operations' description: distributedOptions.description || "External operations",
}); });
if (!externalResult.success && rollbackDatabaseOnExternalFailure) { if (!externalResult.success && rollbackDatabaseOnExternalFailure) {
// Note: Database transaction already committed, so we can't rollback automatically // 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 is a limitation of this approach - consider using saga pattern for true rollback
this.logger.error(`External operations failed but database already committed [${transactionId}]`, { this.logger.error(
externalError: externalResult.error `External operations failed but database already committed [${transactionId}]`,
}); {
externalError: externalResult.error,
}
);
} }
} else { } else {
// Execute external operations first // Execute external operations first
this.logger.debug(`Executing external operations [${transactionId}]`); this.logger.debug(`Executing external operations [${transactionId}]`);
externalResult = await this.executeDistributedTransaction(externalSteps, { externalResult = await this.executeDistributedTransaction(externalSteps, {
...distributedOptions, ...distributedOptions,
description: distributedOptions.description || 'External operations' description: distributedOptions.description || "External operations",
}); });
if (!externalResult.success) { if (!externalResult.success) {
throw new Error(externalResult.error || 'External operations failed'); throw new Error(externalResult.error || "External operations failed");
} }
// Execute database operations // Execute database operations
@ -283,7 +285,7 @@ export class DistributedTransactionService {
databaseOperation, databaseOperation,
{ {
description: `${options.description} - Database Operations`, description: `${options.description} - Database Operations`,
timeout: options.timeout timeout: options.timeout,
} }
); );
@ -295,7 +297,7 @@ export class DistributedTransactionService {
externalResult.stepResults, externalResult.stepResults,
transactionId transactionId
); );
throw new Error(dbTransactionResult.error || 'Database transaction failed'); throw new Error(dbTransactionResult.error || "Database transaction failed");
} }
databaseResult = dbTransactionResult.data!; databaseResult = dbTransactionResult.data!;
@ -305,7 +307,7 @@ export class DistributedTransactionService {
this.logger.log(`Hybrid transaction completed successfully [${transactionId}]`, { this.logger.log(`Hybrid transaction completed successfully [${transactionId}]`, {
description: options.description, description: options.description,
duration duration,
}); });
return { return {
@ -315,16 +317,15 @@ export class DistributedTransactionService {
stepsExecuted: externalResult?.stepsExecuted || 0, stepsExecuted: externalResult?.stepsExecuted || 0,
stepsRolledBack: 0, stepsRolledBack: 0,
stepResults: externalResult?.stepResults || {}, stepResults: externalResult?.stepResults || {},
failedSteps: externalResult?.failedSteps || [] failedSteps: externalResult?.failedSteps || [],
}; };
} catch (error) { } catch (error) {
const duration = Date.now() - startTime; const duration = Date.now() - startTime;
this.logger.error(`Hybrid transaction failed [${transactionId}]`, { this.logger.error(`Hybrid transaction failed [${transactionId}]`, {
description: options.description, description: options.description,
error: getErrorMessage(error), error: getErrorMessage(error),
duration duration,
}); });
return { return {
@ -334,7 +335,7 @@ export class DistributedTransactionService {
stepsExecuted: 0, stepsExecuted: 0,
stepsRolledBack: 0, stepsRolledBack: 0,
stepResults: {}, stepResults: {},
failedSteps: [] failedSteps: [],
}; };
} }
} }
@ -346,7 +347,7 @@ export class DistributedTransactionService {
setTimeout(() => { setTimeout(() => {
reject(new Error(`Step ${step.id} timed out after ${timeout}ms`)); reject(new Error(`Step ${step.id} timed out after ${timeout}ms`));
}, timeout); }, timeout);
}) }),
]); ]);
} }
@ -373,7 +374,7 @@ export class DistributedTransactionService {
this.logger.debug(`Rollback completed for step: ${stepId} [${transactionId}]`); this.logger.debug(`Rollback completed for step: ${stepId} [${transactionId}]`);
} catch (rollbackError) { } catch (rollbackError) {
this.logger.error(`Rollback failed for step: ${stepId} [${transactionId}]`, { this.logger.error(`Rollback failed for step: ${stepId} [${transactionId}]`, {
error: getErrorMessage(rollbackError) error: getErrorMessage(rollbackError),
}); });
// Continue with other rollbacks even if one fails // Continue with other rollbacks even if one fails
} }

View File

@ -27,7 +27,7 @@ export interface TransactionOptions {
* Custom isolation level for the transaction * Custom isolation level for the transaction
* Default: ReadCommitted * Default: ReadCommitted
*/ */
isolationLevel?: 'ReadUncommitted' | 'ReadCommitted' | 'RepeatableRead' | 'Serializable'; isolationLevel?: "ReadUncommitted" | "ReadCommitted" | "RepeatableRead" | "Serializable";
/** /**
* Description of the transaction for logging * Description of the transaction for logging
@ -102,9 +102,9 @@ export class TransactionService {
const { const {
timeout = this.defaultTimeout, timeout = this.defaultTimeout,
maxRetries = this.defaultMaxRetries, maxRetries = this.defaultMaxRetries,
isolationLevel = 'ReadCommitted', isolationLevel = "ReadCommitted",
description = 'Database transaction', description = "Database transaction",
autoRollback = true autoRollback = true,
} = options; } = options;
const transactionId = this.generateTransactionId(); const transactionId = this.generateTransactionId();
@ -114,14 +114,14 @@ export class TransactionService {
id: transactionId, id: transactionId,
startTime, startTime,
operations: [], operations: [],
rollbackActions: [] rollbackActions: [],
}; };
this.logger.log(`Starting transaction [${transactionId}]`, { this.logger.log(`Starting transaction [${transactionId}]`, {
description, description,
timeout, timeout,
isolationLevel, isolationLevel,
maxRetries maxRetries,
}); });
let attempt = 0; let attempt = 0;
@ -137,13 +137,13 @@ export class TransactionService {
id: transactionId, id: transactionId,
startTime, startTime,
operations: [], operations: [],
rollbackActions: [] rollbackActions: [],
}; };
} }
const result = await Promise.race([ const result = await Promise.race([
this.executeTransactionAttempt(operation, context, isolationLevel), this.executeTransactionAttempt(operation, context, isolationLevel),
this.createTimeoutPromise<T>(timeout, transactionId) this.createTimeoutPromise<T>(timeout, transactionId),
]); ]);
const duration = Date.now() - startTime.getTime(); const duration = Date.now() - startTime.getTime();
@ -152,7 +152,7 @@ export class TransactionService {
description, description,
duration, duration,
attempt, attempt,
operationsCount: context.operations.length operationsCount: context.operations.length,
}); });
return { return {
@ -160,9 +160,8 @@ export class TransactionService {
data: result, data: result,
duration, duration,
operationsCount: context.operations.length, operationsCount: context.operations.length,
rollbacksExecuted: 0 rollbacksExecuted: 0,
}; };
} catch (error) { } catch (error) {
lastError = error as Error; lastError = error as Error;
const duration = Date.now() - startTime.getTime(); const duration = Date.now() - startTime.getTime();
@ -172,7 +171,7 @@ export class TransactionService {
error: getErrorMessage(error), error: getErrorMessage(error),
duration, duration,
operationsCount: context.operations.length, 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 // Execute rollbacks if this is the final attempt or not a retryable error
@ -184,7 +183,7 @@ export class TransactionService {
error: getErrorMessage(error), error: getErrorMessage(error),
duration, duration,
operationsCount: context.operations.length, operationsCount: context.operations.length,
rollbacksExecuted rollbacksExecuted,
}; };
} }
@ -197,10 +196,10 @@ export class TransactionService {
const duration = Date.now() - startTime.getTime(); const duration = Date.now() - startTime.getTime();
return { return {
success: false, success: false,
error: lastError ? getErrorMessage(lastError) : 'Unknown transaction error', error: lastError ? getErrorMessage(lastError) : "Unknown transaction error",
duration, duration,
operationsCount: context.operations.length, operationsCount: context.operations.length,
rollbacksExecuted: 0 rollbacksExecuted: 0,
}; };
} }
@ -209,15 +208,15 @@ export class TransactionService {
*/ */
async executeSimpleTransaction<T>( async executeSimpleTransaction<T>(
operation: (tx: any) => Promise<T>, operation: (tx: any) => Promise<T>,
options: Omit<TransactionOptions, 'autoRollback'> = {} options: Omit<TransactionOptions, "autoRollback"> = {}
): Promise<T> { ): Promise<T> {
const result = await this.executeTransaction( const result = await this.executeTransaction(async (tx, _context) => operation(tx), {
async (tx, _context) => operation(tx), ...options,
{ ...options, autoRollback: false } autoRollback: false,
); });
if (!result.success) { if (!result.success) {
throw new Error(result.error || 'Transaction failed'); throw new Error(result.error || "Transaction failed");
} }
return result.data!; return result.data!;
@ -229,7 +228,7 @@ export class TransactionService {
isolationLevel: string isolationLevel: string
): Promise<T> { ): Promise<T> {
return await this.prisma.$transaction( return await this.prisma.$transaction(
async (tx) => { async tx => {
// Enhance context with helper methods // Enhance context with helper methods
const enhancedContext = this.enhanceContext(context); const enhancedContext = this.enhanceContext(context);
@ -238,7 +237,7 @@ export class TransactionService {
}, },
{ {
isolationLevel: isolationLevel as any, 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>) => { addRollback: (rollbackFn: () => Promise<void>) => {
context.rollbackActions.push(rollbackFn); context.rollbackActions.push(rollbackFn);
} },
} as TransactionContext & { } as TransactionContext & {
addOperation: (description: string) => void; addOperation: (description: string) => void;
addRollback: (rollbackFn: () => Promise<void>) => void; addRollback: (rollbackFn: () => Promise<void>) => void;
@ -266,7 +265,9 @@ export class TransactionService {
return 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; let rollbacksExecuted = 0;
@ -278,13 +279,15 @@ export class TransactionService {
this.logger.debug(`Rollback ${i + 1} completed [${context.id}]`); this.logger.debug(`Rollback ${i + 1} completed [${context.id}]`);
} catch (rollbackError) { } catch (rollbackError) {
this.logger.error(`Rollback ${i + 1} failed [${context.id}]`, { this.logger.error(`Rollback ${i + 1} failed [${context.id}]`, {
error: getErrorMessage(rollbackError) error: getErrorMessage(rollbackError),
}); });
// Continue with other rollbacks even if one fails // 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; return rollbacksExecuted;
} }
@ -293,11 +296,11 @@ export class TransactionService {
// Retry on serialization failures, deadlocks, and temporary connection issues // Retry on serialization failures, deadlocks, and temporary connection issues
return ( return (
errorMessage.includes('serialization failure') || errorMessage.includes("serialization failure") ||
errorMessage.includes('deadlock') || errorMessage.includes("deadlock") ||
errorMessage.includes('connection') || errorMessage.includes("connection") ||
errorMessage.includes('timeout') || errorMessage.includes("timeout") ||
errorMessage.includes('lock wait timeout') errorMessage.includes("lock wait timeout")
); );
} }
@ -326,7 +329,7 @@ export class TransactionService {
activeTransactions: 0, // Would need to track active transactions activeTransactions: 0, // Would need to track active transactions
totalTransactions: 0, // Would need to track total count totalTransactions: 0, // Would need to track total count
successRate: 0, // Would need to track success/failure rates 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

@ -14,11 +14,11 @@ export class QueueHealthController {
@Get() @Get()
@ApiOperation({ @ApiOperation({
summary: "Get queue health status", 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({ @ApiResponse({
status: 200, status: 200,
description: "Queue health status retrieved successfully" description: "Queue health status retrieved successfully",
}) })
getQueueHealth() { getQueueHealth() {
return { return {
@ -38,11 +38,11 @@ export class QueueHealthController {
@Get("whmcs") @Get("whmcs")
@ApiOperation({ @ApiOperation({
summary: "Get WHMCS queue metrics", summary: "Get WHMCS queue metrics",
description: "Returns detailed metrics for the WHMCS request queue" description: "Returns detailed metrics for the WHMCS request queue",
}) })
@ApiResponse({ @ApiResponse({
status: 200, status: 200,
description: "WHMCS queue metrics retrieved successfully" description: "WHMCS queue metrics retrieved successfully",
}) })
getWhmcsQueueMetrics() { getWhmcsQueueMetrics() {
return { return {
@ -55,11 +55,12 @@ export class QueueHealthController {
@Get("salesforce") @Get("salesforce")
@ApiOperation({ @ApiOperation({
summary: "Get Salesforce queue metrics", 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({ @ApiResponse({
status: 200, status: 200,
description: "Salesforce queue metrics retrieved successfully" description: "Salesforce queue metrics retrieved successfully",
}) })
getSalesforceQueueMetrics() { getSalesforceQueueMetrics() {
return { return {

View File

@ -7,7 +7,6 @@ import {
Inject, Inject,
} from "@nestjs/common"; } from "@nestjs/common";
import { Request, Response } from "express"; import { Request, Response } from "express";
import { getClientSafeErrorMessage } from "../utils/error.util";
import { Logger } from "nestjs-pino"; import { Logger } from "nestjs-pino";
import { ConfigService } from "@nestjs/config"; import { ConfigService } from "@nestjs/config";
import { SecureErrorMapperService } from "../security/services/secure-error-mapper.service"; import { SecureErrorMapperService } from "../security/services/secure-error-mapper.service";
@ -48,7 +47,8 @@ export class GlobalExceptionFilter implements ExceptionFilter {
const errorResponse = exceptionResponse as { message?: string; error?: string }; const errorResponse = exceptionResponse as { message?: string; error?: string };
originalError = errorResponse.message || exception.message; originalError = errorResponse.message || exception.message;
} else { } else {
originalError = typeof exceptionResponse === "string" ? exceptionResponse : exception.message; originalError =
typeof exceptionResponse === "string" ? exceptionResponse : exception.message;
} }
} else { } else {
status = HttpStatus.INTERNAL_SERVER_ERROR; status = HttpStatus.INTERNAL_SERVER_ERROR;
@ -62,7 +62,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
// Log the error securely (this handles sensitive data filtering) // Log the error securely (this handles sensitive data filtering)
this.secureErrorMapper.logSecureError(originalError, errorContext, { this.secureErrorMapper.logSecureError(originalError, errorContext, {
httpStatus: status, httpStatus: status,
exceptionType: exception instanceof Error ? exception.constructor.name : 'Unknown' exceptionType: exception instanceof Error ? exception.constructor.name : "Unknown",
}); });
// Create secure error response // Create secure error response

View File

@ -110,10 +110,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
// Wait for pending requests to complete (with timeout) // Wait for pending requests to complete (with timeout)
try { try {
await Promise.all([ await Promise.all([this.standardQueue.onIdle(), this.longRunningQueue.onIdle()]);
this.standardQueue.onIdle(),
this.longRunningQueue.onIdle(),
]);
} catch (error) { } catch (error) {
this.logger.warn("Some Salesforce requests may not have completed during shutdown", { this.logger.warn("Some Salesforce requests may not have completed during shutdown", {
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
@ -219,10 +216,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
/** /**
* Execute high-priority Salesforce request (jumps queue) * Execute high-priority Salesforce request (jumps queue)
*/ */
async executeHighPriority<T>( async executeHighPriority<T>(requestFn: () => Promise<T>, isLongRunning = false): Promise<T> {
requestFn: () => Promise<T>,
isLongRunning = false
): Promise<T> {
return this.execute(requestFn, { priority: 10, isLongRunning }); return this.execute(requestFn, { priority: 10, isLongRunning });
} }
@ -254,9 +248,8 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
} { } {
this.updateQueueMetrics(); this.updateQueueMetrics();
const errorRate = this.metrics.totalRequests > 0 const errorRate =
? this.metrics.failedRequests / this.metrics.totalRequests this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
: 0;
// Estimate daily limit (conservative: 150,000 for ~50 users) // Estimate daily limit (conservative: 150,000 for ~50 users)
const estimatedDailyLimit = 150000; const estimatedDailyLimit = 150000;
@ -265,17 +258,9 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
let status: "healthy" | "degraded" | "unhealthy" = "healthy"; let status: "healthy" | "degraded" | "unhealthy" = "healthy";
// Adjusted thresholds for higher throughput (15 concurrent, 10 RPS) // Adjusted thresholds for higher throughput (15 concurrent, 10 RPS)
if ( if (this.metrics.queueSize > 200 || errorRate > 0.1 || dailyUsagePercent > 0.9) {
this.metrics.queueSize > 200 ||
errorRate > 0.1 ||
dailyUsagePercent > 0.9
) {
status = "unhealthy"; status = "unhealthy";
} else if ( } else if (this.metrics.queueSize > 80 || errorRate > 0.05 || dailyUsagePercent > 0.7) {
this.metrics.queueSize > 80 ||
errorRate > 0.05 ||
dailyUsagePercent > 0.7
) {
status = "degraded"; status = "degraded";
} }
@ -320,10 +305,7 @@ export class SalesforceRequestQueueService implements OnModuleInit, OnModuleDest
this.standardQueue.clear(); this.standardQueue.clear();
this.longRunningQueue.clear(); this.longRunningQueue.clear();
await Promise.all([ await Promise.all([this.standardQueue.onIdle(), this.longRunningQueue.onIdle()]);
this.standardQueue.onIdle(),
this.longRunningQueue.onIdle(),
]);
} }
private async executeWithRetry<T>( private async executeWithRetry<T>(

View File

@ -100,10 +100,7 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
/** /**
* Execute a WHMCS API request through the queue * Execute a WHMCS API request through the queue
*/ */
async execute<T>( async execute<T>(requestFn: () => Promise<T>, options: WhmcsRequestOptions = {}): Promise<T> {
requestFn: () => Promise<T>,
options: WhmcsRequestOptions = {}
): Promise<T> {
await this.initializeQueue(); await this.initializeQueue();
const startTime = Date.now(); const startTime = Date.now();
const requestId = this.generateRequestId(); const requestId = this.generateRequestId();
@ -200,9 +197,8 @@ export class WhmcsRequestQueueService implements OnModuleInit, OnModuleDestroy {
} { } {
this.updateQueueMetrics(); this.updateQueueMetrics();
const errorRate = this.metrics.totalRequests > 0 const errorRate =
? this.metrics.failedRequests / this.metrics.totalRequests this.metrics.totalRequests > 0 ? this.metrics.failedRequests / this.metrics.totalRequests : 0;
: 0;
let status: "healthy" | "degraded" | "unhealthy" = "healthy"; let status: "healthy" | "degraded" | "unhealthy" = "healthy";

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

View File

@ -29,16 +29,16 @@ export class CsrfMiddleware implements NestMiddleware {
// Paths that don't require CSRF protection // Paths that don't require CSRF protection
this.exemptPaths = new Set([ this.exemptPaths = new Set([
'/api/auth/login', "/api/auth/login",
'/api/auth/signup', "/api/auth/signup",
'/api/auth/refresh', "/api/auth/refresh",
'/api/health', "/api/health",
'/docs', "/docs",
'/api/webhooks', // Webhooks typically don't use CSRF "/api/webhooks", // Webhooks typically don't use CSRF
]); ]);
// Methods that don't require CSRF protection (safe methods) // 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 { 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 // Check for API endpoints that might be exempt
if (req.path.startsWith('/api/webhooks/')) { if (req.path.startsWith("/api/webhooks/")) {
return true; return true;
} }
@ -77,7 +77,7 @@ export class CsrfMiddleware implements NestMiddleware {
private requiresCsrfProtection(req: CsrfRequest): boolean { private requiresCsrfProtection(req: CsrfRequest): boolean {
// State-changing methods require CSRF protection // 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 { 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", { this.logger.warn("CSRF validation failed - missing token", {
method: req.method, method: req.method,
path: req.path, path: req.path,
userAgent: req.get('user-agent'), userAgent: req.get("user-agent"),
ip: req.ip ip: req.ip,
}); });
throw new ForbiddenException("CSRF token required"); throw new ForbiddenException("CSRF token required");
} }
@ -100,23 +100,28 @@ export class CsrfMiddleware implements NestMiddleware {
this.logger.warn("CSRF validation failed - missing secret cookie", { this.logger.warn("CSRF validation failed - missing secret cookie", {
method: req.method, method: req.method,
path: req.path, path: req.path,
userAgent: req.get('user-agent'), userAgent: req.get("user-agent"),
ip: req.ip ip: req.ip,
}); });
throw new ForbiddenException("CSRF secret required"); 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) { if (!validationResult.isValid) {
this.logger.warn("CSRF validation failed", { this.logger.warn("CSRF validation failed", {
reason: validationResult.reason, reason: validationResult.reason,
method: req.method, method: req.method,
path: req.path, path: req.path,
userAgent: req.get('user-agent'), userAgent: req.get("user-agent"),
ip: req.ip, ip: req.ip,
userId, userId,
sessionId sessionId,
}); });
throw new ForbiddenException(`CSRF validation failed: ${validationResult.reason}`); throw new ForbiddenException(`CSRF validation failed: ${validationResult.reason}`);
} }
@ -128,7 +133,7 @@ export class CsrfMiddleware implements NestMiddleware {
method: req.method, method: req.method,
path: req.path, path: req.path,
userId, userId,
sessionId sessionId,
}); });
next(); next();
@ -151,13 +156,13 @@ export class CsrfMiddleware implements NestMiddleware {
this.setCsrfSecretCookie(res, tokenData.secret); this.setCsrfSecretCookie(res, tokenData.secret);
// Set CSRF token in response header for client to use // 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", { this.logger.debug("CSRF token generated and set", {
method: req.method, method: req.method,
path: req.path, path: req.path,
userId, userId,
sessionId sessionId,
}); });
next(); next();
@ -167,28 +172,28 @@ export class CsrfMiddleware implements NestMiddleware {
// Check multiple possible locations for the CSRF token // Check multiple possible locations for the CSRF token
// 1. X-CSRF-Token header (most common) // 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; if (token) return token;
// 2. X-Requested-With header (alternative) // 2. X-Requested-With header (alternative)
token = req.get('X-Requested-With'); token = req.get("X-Requested-With");
if (token && token !== 'XMLHttpRequest') return token; if (token && token !== "XMLHttpRequest") return token;
// 3. Authorization header (if using Bearer token pattern) // 3. Authorization header (if using Bearer token pattern)
const authHeader = req.get('Authorization'); const authHeader = req.get("Authorization");
if (authHeader && authHeader.startsWith('CSRF ')) { if (authHeader && authHeader.startsWith("CSRF ")) {
return authHeader.substring(5); return authHeader.substring(5);
} }
// 4. Request body (for form submissions) // 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; token = req.body._csrf || req.body.csrfToken;
if (token) return token; if (token) return token;
} }
// 5. Query parameter (least secure, only for GET requests) // 5. Query parameter (least secure, only for GET requests)
if (req.method === 'GET') { if (req.method === "GET") {
token = req.query._csrf as string || req.query.csrfToken as string; token = (req.query._csrf as string) || (req.query.csrfToken as string);
if (token) return token; if (token) return token;
} }
@ -196,26 +201,23 @@ export class CsrfMiddleware implements NestMiddleware {
} }
private extractSecretFromCookie(req: CsrfRequest): string | null { private extractSecretFromCookie(req: CsrfRequest): string | null {
return req.cookies?.['csrf-secret'] || null; return req.cookies?.["csrf-secret"] || null;
} }
private extractSessionId(req: CsrfRequest): string | null { private extractSessionId(req: CsrfRequest): string | null {
// Try to extract session ID from various sources // Try to extract session ID from various sources
return req.cookies?.['session-id'] || return req.cookies?.["session-id"] || req.cookies?.["connect.sid"] || req.sessionID || null;
req.cookies?.['connect.sid'] ||
req.sessionID ||
null;
} }
private setCsrfSecretCookie(res: Response, secret: string): void { private setCsrfSecretCookie(res: Response, secret: string): void {
const cookieOptions = { const cookieOptions = {
httpOnly: true, httpOnly: true,
secure: this.isProduction, secure: this.isProduction,
sameSite: 'strict' as const, sameSite: "strict" as const,
maxAge: 3600000, // 1 hour 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 { export class SecurityModule implements NestModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {
// Apply CSRF middleware to all routes except those handled by the middleware itself // Apply CSRF middleware to all routes except those handled by the middleware itself
consumer consumer.apply(CsrfMiddleware).forRoutes("*");
.apply(CsrfMiddleware)
.forRoutes('*');
} }
} }

View File

@ -36,7 +36,9 @@ export class CsrfService {
this.secretKey = this.configService.get("CSRF_SECRET_KEY") || this.generateSecretKey(); this.secretKey = this.configService.get("CSRF_SECRET_KEY") || this.generateSecretKey();
if (!this.configService.get("CSRF_SECRET_KEY")) { 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 // Clean up expired tokens periodically
@ -56,7 +58,7 @@ export class CsrfService {
secret, secret,
expiresAt, expiresAt,
sessionId, sessionId,
userId userId,
}; };
// Store in cache for validation // Store in cache for validation
@ -71,7 +73,7 @@ export class CsrfService {
tokenHash: this.hashToken(token), tokenHash: this.hashToken(token),
sessionId, sessionId,
userId, userId,
expiresAt: expiresAt.toISOString() expiresAt: expiresAt.toISOString(),
}); });
return tokenData; return tokenData;
@ -89,7 +91,7 @@ export class CsrfService {
if (!token || !secret) { if (!token || !secret) {
return { return {
isValid: false, isValid: false,
reason: "Missing token or secret" reason: "Missing token or secret",
}; };
} }
@ -98,7 +100,7 @@ export class CsrfService {
if (!cachedTokenData) { if (!cachedTokenData) {
return { return {
isValid: false, 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); this.tokenCache.delete(token);
return { return {
isValid: false, isValid: false,
reason: "Token expired" reason: "Token expired",
}; };
} }
@ -116,11 +118,11 @@ export class CsrfService {
this.logger.warn("CSRF token validation failed - secret mismatch", { this.logger.warn("CSRF token validation failed - secret mismatch", {
tokenHash: this.hashToken(token), tokenHash: this.hashToken(token),
sessionId, sessionId,
userId userId,
}); });
return { return {
isValid: false, isValid: false,
reason: "Invalid secret" reason: "Invalid secret",
}; };
} }
@ -129,11 +131,11 @@ export class CsrfService {
this.logger.warn("CSRF token validation failed - session mismatch", { this.logger.warn("CSRF token validation failed - session mismatch", {
tokenHash: this.hashToken(token), tokenHash: this.hashToken(token),
expectedSession: cachedTokenData.sessionId, expectedSession: cachedTokenData.sessionId,
providedSession: sessionId providedSession: sessionId,
}); });
return { return {
isValid: false, isValid: false,
reason: "Session mismatch" reason: "Session mismatch",
}; };
} }
@ -142,11 +144,11 @@ export class CsrfService {
this.logger.warn("CSRF token validation failed - user mismatch", { this.logger.warn("CSRF token validation failed - user mismatch", {
tokenHash: this.hashToken(token), tokenHash: this.hashToken(token),
expectedUser: cachedTokenData.userId, expectedUser: cachedTokenData.userId,
providedUser: userId providedUser: userId,
}); });
return { return {
isValid: false, isValid: false,
reason: "User mismatch" reason: "User mismatch",
}; };
} }
@ -162,23 +164,23 @@ export class CsrfService {
this.logger.warn("CSRF token validation failed - token mismatch", { this.logger.warn("CSRF token validation failed - token mismatch", {
tokenHash: this.hashToken(token), tokenHash: this.hashToken(token),
sessionId, sessionId,
userId userId,
}); });
return { return {
isValid: false, isValid: false,
reason: "Invalid token" reason: "Invalid token",
}; };
} }
this.logger.debug("CSRF token validated successfully", { this.logger.debug("CSRF token validated successfully", {
tokenHash: this.hashToken(token), tokenHash: this.hashToken(token),
sessionId, sessionId,
userId userId,
}); });
return { return {
isValid: true, isValid: true,
tokenData: cachedTokenData tokenData: cachedTokenData,
}; };
} }
@ -188,7 +190,7 @@ export class CsrfService {
invalidateToken(token: string): void { invalidateToken(token: string): void {
this.tokenCache.delete(token); this.tokenCache.delete(token);
this.logger.debug("CSRF token invalidated", { this.logger.debug("CSRF token invalidated", {
tokenHash: this.hashToken(token) tokenHash: this.hashToken(token),
}); });
} }
@ -206,7 +208,7 @@ export class CsrfService {
this.logger.debug("CSRF tokens invalidated for session", { this.logger.debug("CSRF tokens invalidated for session", {
sessionId, sessionId,
invalidatedCount invalidatedCount,
}); });
} }
@ -224,7 +226,7 @@ export class CsrfService {
this.logger.debug("CSRF tokens invalidated for user", { this.logger.debug("CSRF tokens invalidated for user", {
userId, userId,
invalidatedCount invalidatedCount,
}); });
} }
@ -249,30 +251,32 @@ export class CsrfService {
activeTokens, activeTokens,
expiredTokens, expiredTokens,
cacheSize: this.tokenCache.size, cacheSize: this.tokenCache.size,
maxCacheSize: this.maxCacheSize maxCacheSize: this.maxCacheSize,
}; };
} }
private generateSecret(): string { private generateSecret(): string {
return crypto.randomBytes(32).toString('base64url'); return crypto.randomBytes(32).toString("base64url");
} }
private generateTokenFromSecret(secret: string, sessionId?: string, userId?: string): string { private generateTokenFromSecret(secret: string, sessionId?: string, userId?: string): string {
const data = [secret, sessionId || '', userId || ''].join('|'); const data = [secret, sessionId || "", userId || ""].join("|");
const hmac = crypto.createHmac('sha256', this.secretKey); const hmac = crypto.createHmac("sha256", this.secretKey);
hmac.update(data); hmac.update(data);
return hmac.digest('base64url'); return hmac.digest("base64url");
} }
private generateSecretKey(): string { private generateSecretKey(): string {
const key = crypto.randomBytes(64).toString('base64url'); const key = crypto.randomBytes(64).toString("base64url");
this.logger.warn("Generated CSRF secret key - set CSRF_SECRET_KEY environment variable for production"); this.logger.warn(
"Generated CSRF secret key - set CSRF_SECRET_KEY environment variable for production"
);
return key; return key;
} }
private hashToken(token: string): string { private hashToken(token: string): string {
// Create a hash of the token for logging (never log the actual token) // 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 { private constantTimeEquals(a: string, b: string): boolean {
@ -302,7 +306,7 @@ export class CsrfService {
if (cleanedCount > 0) { if (cleanedCount > 0) {
this.logger.debug("Cleaned up expired CSRF tokens", { this.logger.debug("Cleaned up expired CSRF tokens", {
cleanedCount, cleanedCount,
remainingTokens: this.tokenCache.size remainingTokens: this.tokenCache.size,
}); });
} }
} }

View File

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

View File

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

View File

@ -67,7 +67,8 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
options: WhmcsRequestOptions = {} options: WhmcsRequestOptions = {}
): Promise<T> { ): Promise<T> {
// Wrap the actual request in the queue to prevent race conditions // Wrap the actual request in the queue to prevent race conditions
return this.requestQueue.execute(async () => { return this.requestQueue.execute(
async () => {
try { try {
const config = this.configService.getConfig(); const config = this.configService.getConfig();
const response = await this.httpClient.makeRequest<T>(config, action, params, options); const response = await this.httpClient.makeRequest<T>(config, action, params, options);
@ -87,12 +88,14 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
// Handle general request errors // Handle general request errors
this.errorHandler.handleRequestError(error, action, params); this.errorHandler.handleRequestError(error, action, params);
} }
}, { },
{
priority: this.getRequestPriority(action), priority: this.getRequestPriority(action),
timeout: options.timeout, timeout: options.timeout,
retryAttempts: options.retryAttempts, retryAttempts: options.retryAttempts,
retryDelay: options.retryDelay, retryDelay: options.retryDelay,
}); }
);
} }
/** /**
@ -286,15 +289,11 @@ export class WhmcsConnectionOrchestratorService implements OnModuleInit {
"GetClientDetails", "GetClientDetails",
"GetInvoice", "GetInvoice",
"CapturePayment", "CapturePayment",
"CreateSsoToken" "CreateSsoToken",
]; ];
// Medium priority actions (important but can wait) // Medium priority actions (important but can wait)
const mediumPriorityActions = [ const mediumPriorityActions = ["GetInvoices", "GetClientsProducts", "GetPayMethods"];
"GetInvoices",
"GetClientsProducts",
"GetPayMethods"
];
if (highPriorityActions.includes(action)) { if (highPriorityActions.includes(action)) {
return 8; // High priority return 8; // High priority

View File

@ -85,7 +85,7 @@ export class WhmcsInvoiceService {
this.logger.log( this.logger.log(
`Fetched ${result.invoices.length} invoices for client ${clientId}, page ${page}` `Fetched ${result.invoices.length} invoices for client ${clientId}, page ${page}`
); );
return result as InvoiceList; return result;
} catch (error) { } catch (error) {
this.logger.error(`Failed to fetch invoices for client ${clientId}`, { this.logger.error(`Failed to fetch invoices for client ${clientId}`, {
error: getErrorMessage(error), error: getErrorMessage(error),
@ -136,7 +136,7 @@ export class WhmcsInvoiceService {
); );
const result: InvoiceList = { const result: InvoiceList = {
invoices: invoicesWithItems as Invoice[], invoices: invoicesWithItems,
pagination: invoiceList.pagination, pagination: invoiceList.pagination,
}; };
@ -230,7 +230,7 @@ export class WhmcsInvoiceService {
try { try {
const transformed = this.invoiceTransformer.transformInvoice(whmcsInvoice); const transformed = this.invoiceTransformer.transformInvoice(whmcsInvoice);
const parsed = invoiceSchema.parse(transformed); const parsed = invoiceSchema.parse(transformed);
invoices.push(parsed as Invoice); invoices.push(parsed);
} catch (error) { } catch (error) {
this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, { this.logger.error(`Failed to transform invoice ${whmcsInvoice.id}`, {
error: getErrorMessage(error), 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 type { Request, Response } from "express";
import { Throttle } from "@nestjs/throttler"; import { Throttle } from "@nestjs/throttler";
import { AuthService } from "./auth.service"; import { AuthService } from "./auth.service";
@ -99,10 +109,7 @@ export class AuthController {
@ApiResponse({ status: 409, description: "Customer already has account" }) @ApiResponse({ status: 409, description: "Customer already has account" })
@ApiResponse({ status: 400, description: "Customer number not found" }) @ApiResponse({ status: 400, description: "Customer number not found" })
@ApiResponse({ status: 429, description: "Too many validation attempts" }) @ApiResponse({ status: 429, description: "Too many validation attempts" })
async validateSignup( async validateSignup(@Body() validateData: ValidateSignupRequestInput, @Req() req: Request) {
@Body() validateData: ValidateSignupRequestInput,
@Req() req: Request
) {
return this.authService.validateSignup(validateData, req); 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> { override async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context const request = context
.switchToHttp() .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}`; const route = `${request.method} ${request.route?.path ?? request.url}`;
// Check if the route is marked as public // Check if the route is marked as public

View File

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

View File

@ -109,7 +109,11 @@ export class OrderBuilder {
assignIfString(orderFields, orderField("accessMode", fieldMap), config.accessMode); 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 || {}; const config = body.configurations || {};
assignIfString(orderFields, orderField("simType", fieldMap), config.simType); assignIfString(orderFields, orderField("simType", fieldMap), config.simType);
assignIfString(orderFields, orderField("eid", fieldMap), config.eid); assignIfString(orderFields, orderField("eid", fieldMap), config.eid);
@ -119,7 +123,11 @@ export class OrderBuilder {
assignIfString(orderFields, mnpField("reservationNumber", fieldMap), config.mnpNumber); assignIfString(orderFields, mnpField("reservationNumber", fieldMap), config.mnpNumber);
assignIfString(orderFields, mnpField("expiryDate", fieldMap), config.mnpExpiry); assignIfString(orderFields, mnpField("expiryDate", fieldMap), config.mnpExpiry);
assignIfString(orderFields, mnpField("phoneNumber", fieldMap), config.mnpPhone); 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("portingLastName", fieldMap), config.portingLastName);
assignIfString(orderFields, mnpField("portingFirstName", fieldMap), config.portingFirstName); assignIfString(orderFields, mnpField("portingFirstName", fieldMap), config.portingFirstName);
assignIfString( assignIfString(
@ -133,7 +141,11 @@ export class OrderBuilder {
config.portingFirstNameKatakana config.portingFirstNameKatakana
); );
assignIfString(orderFields, mnpField("portingGender", fieldMap), config.portingGender); 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) { } catch (error) {
this.logger.error("Fulfillment validation failed", { this.logger.error("Fulfillment validation failed", {
sfOrderId, sfOrderId,
error: getErrorMessage(error) error: getErrorMessage(error),
}); });
throw error; throw error;
} }
@ -118,16 +118,18 @@ export class OrderFulfillmentOrchestrator {
} catch (error) { } catch (error) {
this.logger.error("Failed to get order details", { this.logger.error("Failed to get order details", {
sfOrderId, sfOrderId,
error: getErrorMessage(error) error: getErrorMessage(error),
}); });
throw error; throw error;
} }
// Step 3: Execute the main fulfillment workflow as a distributed transaction // Step 3: Execute the main fulfillment workflow as a distributed transaction
const fulfillmentResult = await this.distributedTransactionService.executeDistributedTransaction([ const fulfillmentResult =
await this.distributedTransactionService.executeDistributedTransaction(
[
{ {
id: 'sf_status_update', id: "sf_status_update",
description: 'Update Salesforce order status to Activating', description: "Update Salesforce order status to Activating",
execute: async () => { execute: async () => {
const fields = this.fieldMapService.getFieldMap(); const fields = this.fieldMapService.getFieldMap();
return await this.salesforceService.updateOrder({ return await this.salesforceService.updateOrder({
@ -142,24 +144,22 @@ export class OrderFulfillmentOrchestrator {
[fields.order.activationStatus]: "Failed", [fields.order.activationStatus]: "Failed",
}); });
}, },
critical: true critical: true,
}, },
{ {
id: 'mapping', id: "mapping",
description: 'Map OrderItems to WHMCS format', description: "Map OrderItems to WHMCS format",
execute: async () => { execute: async () => {
if (!context.orderDetails) { if (!context.orderDetails) {
throw new Error("Order details are required for mapping"); throw new Error("Order details are required for mapping");
} }
return this.orderWhmcsMapper.mapOrderItemsToWhmcs( return this.orderWhmcsMapper.mapOrderItemsToWhmcs(context.orderDetails.items);
context.orderDetails.items
);
}, },
critical: true critical: true,
}, },
{ {
id: 'whmcs_create', id: "whmcs_create",
description: 'Create order in WHMCS', description: "Create order in WHMCS",
execute: async () => { execute: async () => {
const mappingResult = fulfillmentResult.stepResults?.mapping; const mappingResult = fulfillmentResult.stepResults?.mapping;
if (!mappingResult) { if (!mappingResult) {
@ -187,47 +187,50 @@ export class OrderFulfillmentOrchestrator {
if (createResult?.orderId) { if (createResult?.orderId) {
// Note: WHMCS doesn't have an automated cancel API // Note: WHMCS doesn't have an automated cancel API
// Manual intervention required for order cleanup // Manual intervention required for order cleanup
this.logger.error("WHMCS order created but fulfillment failed - manual cleanup required", { this.logger.error(
"WHMCS order created but fulfillment failed - manual cleanup required",
{
orderId: createResult.orderId, orderId: createResult.orderId,
sfOrderId, sfOrderId,
action: "MANUAL_CLEANUP_REQUIRED" action: "MANUAL_CLEANUP_REQUIRED",
}); }
);
} }
}, },
critical: true critical: true,
}, },
{ {
id: 'whmcs_accept', id: "whmcs_accept",
description: 'Accept/provision order in WHMCS', description: "Accept/provision order in WHMCS",
execute: async () => { execute: async () => {
const createResult = fulfillmentResult.stepResults?.whmcs_create; const createResult = fulfillmentResult.stepResults?.whmcs_create;
if (!createResult?.orderId) { if (!createResult?.orderId) {
throw new Error("WHMCS order ID missing before acceptance step"); throw new Error("WHMCS order ID missing before acceptance step");
} }
return await this.whmcsOrderService.acceptOrder( return await this.whmcsOrderService.acceptOrder(createResult.orderId, sfOrderId);
createResult.orderId,
sfOrderId
);
}, },
rollback: async () => { rollback: async () => {
const acceptResult = fulfillmentResult.stepResults?.whmcs_accept; const acceptResult = fulfillmentResult.stepResults?.whmcs_accept;
if (acceptResult?.orderId) { if (acceptResult?.orderId) {
// Note: WHMCS doesn't have an automated cancel API for accepted orders // Note: WHMCS doesn't have an automated cancel API for accepted orders
// Manual intervention required for service termination // Manual intervention required for service termination
this.logger.error("WHMCS order accepted but fulfillment failed - manual cleanup required", { this.logger.error(
"WHMCS order accepted but fulfillment failed - manual cleanup required",
{
orderId: acceptResult.orderId, orderId: acceptResult.orderId,
serviceIds: acceptResult.serviceIds, serviceIds: acceptResult.serviceIds,
sfOrderId, sfOrderId,
action: "MANUAL_SERVICE_TERMINATION_REQUIRED" action: "MANUAL_SERVICE_TERMINATION_REQUIRED",
}); }
);
} }
}, },
critical: true critical: true,
}, },
{ {
id: 'sim_fulfillment', id: "sim_fulfillment",
description: 'SIM-specific fulfillment (if applicable)', description: "SIM-specific fulfillment (if applicable)",
execute: async () => { execute: async () => {
if (context.orderDetails?.orderType === "SIM") { if (context.orderDetails?.orderType === "SIM") {
const configurations = this.extractConfigurations(payload.configurations); const configurations = this.extractConfigurations(payload.configurations);
@ -239,11 +242,11 @@ export class OrderFulfillmentOrchestrator {
} }
return { skipped: true }; return { skipped: true };
}, },
critical: false // SIM fulfillment failure shouldn't rollback the entire order critical: false, // SIM fulfillment failure shouldn't rollback the entire order
}, },
{ {
id: 'sf_success_update', id: "sf_success_update",
description: 'Update Salesforce with success', description: "Update Salesforce with success",
execute: async () => { execute: async () => {
const fields = this.fieldMapService.getFieldMap(); const fields = this.fieldMapService.getFieldMap();
const whmcsResult = fulfillmentResult.stepResults?.whmcs_accept; const whmcsResult = fulfillmentResult.stepResults?.whmcs_accept;
@ -262,20 +265,22 @@ export class OrderFulfillmentOrchestrator {
[fields.order.activationStatus]: "Failed", [fields.order.activationStatus]: "Failed",
}); });
}, },
critical: true critical: true,
} },
], { ],
{
description: `Order fulfillment for ${sfOrderId}`, description: `Order fulfillment for ${sfOrderId}`,
timeout: 300000, // 5 minutes timeout: 300000, // 5 minutes
continueOnNonCriticalFailure: true continueOnNonCriticalFailure: true,
}); }
);
if (!fulfillmentResult.success) { if (!fulfillmentResult.success) {
this.logger.error("Fulfillment transaction failed", { this.logger.error("Fulfillment transaction failed", {
sfOrderId, sfOrderId,
error: fulfillmentResult.error, error: fulfillmentResult.error,
stepsExecuted: fulfillmentResult.stepsExecuted, stepsExecuted: fulfillmentResult.stepsExecuted,
stepsRolledBack: fulfillmentResult.stepsRolledBack stepsRolledBack: fulfillmentResult.stepsRolledBack,
}); });
throw new Error(fulfillmentResult.error || "Fulfillment transaction failed"); throw new Error(fulfillmentResult.error || "Fulfillment transaction failed");
} }
@ -287,7 +292,7 @@ export class OrderFulfillmentOrchestrator {
this.logger.log("Transactional fulfillment completed successfully", { this.logger.log("Transactional fulfillment completed successfully", {
sfOrderId, sfOrderId,
stepsExecuted: fulfillmentResult.stepsExecuted, stepsExecuted: fulfillmentResult.stepsExecuted,
duration: fulfillmentResult.duration duration: fulfillmentResult.duration,
}); });
return context; return context;

View File

@ -116,7 +116,9 @@ export class OrderPricebookService {
internetOfferingType: product internetOfferingType: product
? getStringField(product, "internetOfferingType", fields) ? getStringField(product, "internetOfferingType", fields)
: undefined, : undefined,
internetPlanTier: product ? getStringField(product, "internetPlanTier", fields) : undefined, internetPlanTier: product
? getStringField(product, "internetPlanTier", fields)
: undefined,
vpnRegion: product ? getStringField(product, "vpnRegion", 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({ export const refreshTokenRequestSchema = z.object({
refreshToken: z refreshToken: z.string().min(1, "Refresh token is required").optional(),
.string()
.min(1, "Refresh token is required")
.optional(),
deviceId: z.string().optional(), deviceId: z.string().optional(),
}); });

View File

@ -7,5 +7,4 @@
export { z } from "zod"; export { z } from "zod";
// Framework-specific exports // Framework-specific exports
export * from "./nestjs";
export * from "./react"; 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 specifier: ^5.0.8
version: 5.0.8(@types/react@19.1.12)(react@19.1.1) version: 5.0.8(@types/react@19.1.12)(react@19.1.1)
devDependencies: devDependencies:
'@next/bundle-analyzer':
specifier: ^15.5.0
version: 15.5.4
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4.1.12 specifier: ^4.1.12
version: 4.1.13 version: 4.1.13
@ -319,6 +322,9 @@ importers:
typescript: typescript:
specifier: ^5.9.2 specifier: ^5.9.2
version: 5.9.2 version: 5.9.2
webpack-bundle-analyzer:
specifier: ^4.10.2
version: 4.10.2
packages/domain: packages/domain:
dependencies: dependencies:
@ -576,6 +582,10 @@ packages:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'} engines: {node: '>=12'}
'@discoveryjs/json-ext@0.5.7':
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
'@emnapi/core@1.5.0': '@emnapi/core@1.5.0':
resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} 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 '@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 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': '@next/env@15.5.0':
resolution: {integrity: sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==} resolution: {integrity: sha512-sDaprBAfzCQiOgo2pO+LhnV0Wt2wBgartjrr+dpcTORYVnnXD0gwhHhiiyIih9hQbq+JnbqH4odgcFWhqCGidw==}
@ -1474,6 +1487,9 @@ packages:
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} 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': '@prisma/client@6.16.0':
resolution: {integrity: sha512-FYkFJtgwpwJRMxtmrB26y7gtpR372kyChw6lWng5TMmvn5V+uisy0OyllO5EJD1s8lX78V8X3XjhiXOoMLnu3w==} resolution: {integrity: sha512-FYkFJtgwpwJRMxtmrB26y7gtpR372kyChw6lWng5TMmvn5V+uisy0OyllO5EJD1s8lX78V8X3XjhiXOoMLnu3w==}
engines: {node: '>=18.18'} engines: {node: '>=18.18'}
@ -2476,6 +2492,10 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
comment-json@4.2.5: comment-json@4.2.5:
resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@ -2593,6 +2613,9 @@ packages:
dateformat@4.6.3: dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
debounce@1.2.1:
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
debug@3.2.7: debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies: peerDependencies:
@ -2693,6 +2716,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
eastasianwidth@0.2.0: eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -3212,6 +3238,10 @@ packages:
graphemer@1.4.0: graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 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: handlebars@4.7.8:
resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==}
engines: {node: '>=0.4.7'} engines: {node: '>=0.4.7'}
@ -3423,6 +3453,10 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'} 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: is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
@ -4000,6 +4034,10 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
hasBin: true hasBin: true
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -4194,6 +4232,10 @@ packages:
openapi-typescript-helpers@0.0.15: openapi-typescript-helpers@0.0.15:
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
opener@1.5.2:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
hasBin: true
optionator@0.9.4: optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -4676,6 +4718,10 @@ packages:
simple-swizzle@0.2.2: simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 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: slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -4926,6 +4972,10 @@ packages:
resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
tough-cookie@6.0.0: tough-cookie@6.0.0:
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -5159,6 +5209,16 @@ packages:
webidl-conversions@3.0.1: webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} 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: webpack-node-externals@3.0.0:
resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -5238,6 +5298,18 @@ packages:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} 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: xml2js@0.6.2:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'} engines: {node: '>=4.0.0'}
@ -5543,6 +5615,8 @@ snapshots:
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.9 '@jridgewell/trace-mapping': 0.3.9
'@discoveryjs/json-ext@0.5.7': {}
'@emnapi/core@1.5.0': '@emnapi/core@1.5.0':
dependencies: dependencies:
'@emnapi/wasi-threads': 1.1.0 '@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) '@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 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/env@15.5.0': {}
'@next/eslint-plugin-next@15.5.0': '@next/eslint-plugin-next@15.5.0':
@ -6426,6 +6507,8 @@ snapshots:
'@pkgr/core@0.2.9': {} '@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)': '@prisma/client@6.16.0(prisma@6.16.0(typescript@5.9.2))(typescript@5.9.2)':
optionalDependencies: optionalDependencies:
prisma: 6.16.0(typescript@5.9.2) prisma: 6.16.0(typescript@5.9.2)
@ -7521,6 +7604,8 @@ snapshots:
commander@4.1.1: {} commander@4.1.1: {}
commander@7.2.0: {}
comment-json@4.2.5: comment-json@4.2.5:
dependencies: dependencies:
array-timsort: 1.0.3 array-timsort: 1.0.3
@ -7631,6 +7716,8 @@ snapshots:
dateformat@4.6.3: {} dateformat@4.6.3: {}
debounce@1.2.1: {}
debug@3.2.7: debug@3.2.7:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@ -7702,6 +7789,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
gopd: 1.2.0 gopd: 1.2.0
duplexer@0.1.2: {}
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11: ecdsa-sig-formatter@1.0.11:
@ -8441,6 +8530,10 @@ snapshots:
graphemer@1.4.0: {} graphemer@1.4.0: {}
gzip-size@6.0.0:
dependencies:
duplexer: 0.1.2
handlebars@4.7.8: handlebars@4.7.8:
dependencies: dependencies:
minimist: 1.2.8 minimist: 1.2.8
@ -8660,6 +8753,8 @@ snapshots:
is-number@7.0.0: {} is-number@7.0.0: {}
is-plain-object@5.0.0: {}
is-promise@4.0.0: {} is-promise@4.0.0: {}
is-regex@1.2.1: is-regex@1.2.1:
@ -9390,6 +9485,8 @@ snapshots:
mkdirp@3.0.1: {} mkdirp@3.0.1: {}
mrmime@2.0.1: {}
ms@2.1.3: {} ms@2.1.3: {}
msgpackr-extract@3.0.3: msgpackr-extract@3.0.3:
@ -9584,6 +9681,8 @@ snapshots:
openapi-typescript-helpers@0.0.15: {} openapi-typescript-helpers@0.0.15: {}
opener@1.5.2: {}
optionator@0.9.4: optionator@0.9.4:
dependencies: dependencies:
deep-is: 0.1.4 deep-is: 0.1.4
@ -10163,6 +10262,12 @@ snapshots:
dependencies: dependencies:
is-arrayish: 0.3.2 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: {} slash@3.0.0: {}
sonic-boom@4.2.0: sonic-boom@4.2.0:
@ -10421,6 +10526,8 @@ snapshots:
'@tokenizer/token': 0.3.0 '@tokenizer/token': 0.3.0
ieee754: 1.2.1 ieee754: 1.2.1
totalist@3.0.1: {}
tough-cookie@6.0.0: tough-cookie@6.0.0:
dependencies: dependencies:
tldts: 7.0.13 tldts: 7.0.13
@ -10672,6 +10779,43 @@ snapshots:
webidl-conversions@3.0.1: {} 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-node-externals@3.0.0: {}
webpack-sources@3.3.3: {} webpack-sources@3.3.3: {}
@ -10797,6 +10941,8 @@ snapshots:
imurmurhash: 0.1.4 imurmurhash: 0.1.4
signal-exit: 4.1.0 signal-exit: 4.1.0
ws@7.5.10: {}
xml2js@0.6.2: xml2js@0.6.2:
dependencies: dependencies:
sax: 1.4.1 sax: 1.4.1

0
rm Normal file
View File

View File

@ -328,9 +328,21 @@ start_apps() {
log "🔨 Building shared package..." log "🔨 Building shared package..."
pnpm --filter @customer-portal/domain build pnpm --filter @customer-portal/domain build
# Build BFF before watch (ensures dist exists) # Build BFF before watch (ensures dist exists). Use Nest build for correct emit.
log "🔨 Building BFF for initial setup..." log "🔨 Building BFF for initial setup (ts emit)..."
(cd "$PROJECT_ROOT/apps/bff" && pnpm tsc -p tsconfig.build.json) (
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 next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}" local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"

0
tsc Normal file
View File