Fix review findings for info broker removal service FRE-5402

P0 fixes:
- Add CANCELLED status to RemovalStatus enum (types + Prisma schema)
- Use CANCELLED instead of REJECTED for user-initiated cancellations
- Add null guard for req.broker?.name in GET /request/:id
- Remove unsafe 'as any' casts in RemoveBrokersService.ts
- Add type-safe toPersonalInfo() validator for JSON deserialization
- Type RemovalRequestWithBroker properly in getRemovalStatus()
- Fix alert: any to NormalizedAlertInput in BrokerAlertPipeline

P1 fixes:
- Fix admin role check: remove non-existent 'admin', only check 'support'
- Fix BrokerDefinition.category type from string to BrokerCategory
- Add complete OpenAPI spec for all removebrokers routes and schemas
This commit is contained in:
2026-05-17 02:30:00 -04:00
parent e9e547be78
commit 7410813f4e
6 changed files with 3673 additions and 11 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -270,7 +270,7 @@ export async function removebrokersRoutes(fastify: FastifyInstance) {
request: {
id: req.id,
brokerId: req.brokerId,
brokerName: req.broker.name,
brokerName: req.broker?.name || null,
status: req.status,
method: req.method,
attempts: req.attempts,
@@ -313,13 +313,13 @@ export async function removebrokersRoutes(fastify: FastifyInstance) {
await prisma.removalRequest.update({
where: { id },
data: { status: RemovalStatus.REJECTED },
data: { status: RemovalStatus.CANCELLED },
});
return reply.send({
request: {
id: req.id,
status: RemovalStatus.REJECTED,
status: RemovalStatus.CANCELLED,
},
});
} catch (error) {
@@ -335,7 +335,7 @@ export async function removebrokersRoutes(fastify: FastifyInstance) {
return reply.code(401).send({ error: 'User not authenticated' });
}
if (authReq.user.role !== 'admin' && authReq.user.role !== 'support') {
if (authReq.user.role !== 'support') {
return reply.code(403).send({ error: 'Admin access required' });
}

View File

@@ -838,6 +838,7 @@ enum RemovalStatus {
COMPLETED
FAILED
REJECTED
CANCELLED
}
model InfoBroker {

View File

@@ -399,6 +399,7 @@ export const RemovalStatus = {
COMPLETED: "COMPLETED",
FAILED: "FAILED",
REJECTED: "REJECTED",
CANCELLED: "CANCELLED",
} as const;
export type RemovalStatus = (typeof RemovalStatus)[keyof typeof RemovalStatus];
@@ -416,7 +417,7 @@ export interface BrokerDefinition {
id: string;
name: string;
domain: string;
category: string;
category: BrokerCategory;
removalMethod: RemovalMethod;
removalUrl?: string;
requiresAccount: boolean;

View File

@@ -1,4 +1,4 @@
import { AlertSource, AlertCategory, Severity, EntityType } from "@shieldai/types";
import { AlertSource, AlertCategory, Severity, EntityType, NormalizedAlertInput } from "@shieldai/types";
export interface BrokerAlertInput {
userId: string;
@@ -55,7 +55,7 @@ export class BrokerAlertPipeline {
return this.normalizeAndSend(alert);
}
private async normalizeAndSend(alert: any) {
private async normalizeAndSend(alert: NormalizedAlertInput) {
try {
const { correlationPipeline } = await import("@shieldai/correlation");
return correlationPipeline.normalizeAlert(alert);

View File

@@ -3,6 +3,34 @@ import { RemovalStatus, RemovalMethod } from "@shieldai/types";
import { getBrokerById, getActiveBrokers } from "./brokerRegistry";
import type { PersonalInfo, RemovalJob, BrokerEntry } from "./types";
import { MAX_REMOVAL_ATTEMPTS, RETRY_DELAY_MS } from "./types";
import type { RemovalRequest as PrismaRemovalRequest, InfoBroker } from "@shieldai/db";
function toPersonalInfo(raw: unknown): PersonalInfo | null {
if (typeof raw !== "object" || raw === null) return null;
const obj = raw as Record<string, unknown>;
if (typeof obj.fullName !== "string") return null;
let address: PersonalInfo["address"] = undefined;
if (typeof obj.address === "object" && obj.address !== null) {
const addr = obj.address as Record<string, unknown>;
address = {
street: typeof addr.street === "string" ? addr.street : undefined,
city: typeof addr.city === "string" ? addr.city : undefined,
state: typeof addr.state === "string" ? addr.state : undefined,
zip: typeof addr.zip === "string" ? addr.zip : undefined,
};
}
return {
fullName: obj.fullName,
email: typeof obj.email === "string" ? obj.email : undefined,
phone: typeof obj.phone === "string" ? obj.phone : undefined,
address,
dob: typeof obj.dob === "string" ? obj.dob : undefined,
};
}
type RemovalRequestWithBroker = PrismaRemovalRequest & {
broker: InfoBroker;
};
export class RemoveBrokersService {
async scanForListings(subscriptionId: string, personalInfo: PersonalInfo) {
@@ -88,7 +116,7 @@ export class RemoveBrokersService {
subscriptionId,
brokerId,
status: RemovalStatus.PENDING,
personalInfo: personalInfo as any,
personalInfo: JSON.parse(JSON.stringify(personalInfo)),
method: broker.removalMethod,
notes,
},
@@ -139,7 +167,7 @@ export class RemoveBrokersService {
requestId: request.id,
brokerId: request.brokerId,
brokerName: getBrokerById(request.brokerId)?.name || request.brokerId,
personalInfo: request.personalInfo as PersonalInfo,
personalInfo: toPersonalInfo(request.personalInfo)!,
method: request.method,
attempt: request.attempts + 1,
};
@@ -203,7 +231,10 @@ export class RemoveBrokersService {
throw new Error(`Removal request not found: ${requestId}`);
}
const personalInfo = request.personalInfo as PersonalInfo;
const personalInfo = toPersonalInfo(request.personalInfo);
if (!personalInfo) {
throw new Error(`Invalid personal info in request ${requestId}`);
}
const stillListed = await this.checkBrokerListing(
request.broker,
personalInfo,
@@ -242,7 +273,7 @@ export class RemoveBrokersService {
orderBy: { updatedAt: "desc" },
});
return requests.map((r: any) => ({
return requests.map((r: RemovalRequestWithBroker) => ({
id: r.id,
brokerId: r.brokerId,
brokerName: r.broker.name,