Auto-commit 2026-05-02 09:37

This commit is contained in:
2026-05-02 09:37:34 -04:00
parent b7600fa937
commit 35d004cde3
3809 changed files with 2315945 additions and 106 deletions

View File

@@ -10,6 +10,8 @@
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"@shieldsai/shared-analytics": "*",
"@shieldsai/shared-billing": "*",
"@shieldsai/shared-db": "*",
"@shieldsai/shared-utils": "*",
"bullmq": "^5.1.0",

View File

@@ -0,0 +1,173 @@
import { prisma, SubscriptionTier } from '@shieldsai/shared-db';
import { Queue, Worker, Job } from 'bullmq';
import { Redis } from 'ioredis';
import { tierConfig, getTierFeatures } from '@shieldsai/shared-billing';
import { mixpanelService, EventType } from '@shieldsai/shared-analytics';
const redisHost = process.env.REDIS_HOST || 'localhost';
const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10);
const connection = new Redis({
host: redisHost,
port: redisPort,
retryStrategy: (times: number) => Math.min(times * 50, 2000),
});
const QUEUE_CONFIG = {
darkwatchScan: {
name: 'darkwatch-scan',
concurrency: parseInt(process.env.DARKWATCH_CONCURRENCY || '5', 10),
defaultJobTimeout: parseInt(process.env.DARKWATCH_JOB_TIMEOUT || '120000', 10),
maxAttempts: parseInt(process.env.DARKWATCH_MAX_ATTEMPTS || '3', 10),
},
};
export const darkwatchScanQueue = new Queue(
QUEUE_CONFIG.darkwatchScan.name,
{ connection }
);
async function processDarkwatchScan(
job: Job<{
subscriptionId: string;
tier: string;
scanType: 'scheduled' | 'on-demand' | 'realtime';
sourceData?: Record<string, unknown>;
}>
) {
const { subscriptionId, tier, scanType, sourceData } = job.data;
const { scanService } = await import(
'../../../apps/api/src/services/darkwatch/scan.service'
);
const { alertPipeline } = await import(
'../../../apps/api/src/services/darkwatch/alert.pipeline'
);
job.updateProgress(10);
console.log(
`[DarkWatch:Scan] Starting ${scanType} scan for subscription ${subscriptionId} (tier: ${tier})`
);
try {
const subscription = await prisma.subscription.findUnique({
where: { id: subscriptionId },
select: { userId: true, tier: true },
});
if (!subscription) {
job.updateProgress(100);
return { status: 'skipped', reason: 'subscription_not_found' };
}
await mixpanelService.track(
EventType.DARK_WEB_SCAN_STARTED,
subscription.userId,
{
scanType,
subscriptionTier: subscription.tier,
}
);
job.updateProgress(25);
const watchlistItems = await scanService.getWatchlistItems(subscriptionId);
if (watchlistItems.length === 0) {
job.updateProgress(100);
return { status: 'completed', exposuresCreated: 0, exposuresUpdated: 0 };
}
job.updateProgress(50);
const { exposuresCreated, exposuresUpdated } =
await scanService.processSubscriptionScan(subscriptionId, watchlistItems);
job.updateProgress(80);
const newExposureIds = await prisma.exposure.findMany({
where: {
subscriptionId,
isFirstTime: true,
detectedAt: { gte: new Date(Date.now() - 5 * 60 * 1000) },
},
select: { id: true },
});
if (newExposureIds.length > 0) {
await alertPipeline.processNewExposures(newExposureIds.map((e) => e.id));
}
await alertPipeline.dispatchScanCompleteAlert(
subscriptionId,
subscription.userId,
exposuresCreated
);
job.updateProgress(95);
await mixpanelService.track(
EventType.DARK_WEB_SCAN_COMPLETED,
subscription.userId,
{
scanType,
subscriptionTier: subscription.tier,
exposuresCreated,
exposuresUpdated,
watchlistItemsScanned: watchlistItems.length,
}
);
job.updateProgress(100);
return {
status: 'completed',
exposuresCreated,
exposuresUpdated,
watchlistItemsScanned: watchlistItems.length,
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Scan failed';
console.error(`[DarkWatch:Scan] Job ${job.id} failed:`, message);
job.updateProgress(100);
throw new Error(message);
}
}
export const darkwatchScanWorker = new Worker(
QUEUE_CONFIG.darkwatchScan.name,
processDarkwatchScan,
{
connection,
concurrency: QUEUE_CONFIG.darkwatchScan.concurrency,
limiter: {
max: 20,
duration: 1000,
},
removeOnComplete: {
age: 7 * 24 * 60 * 60,
count: 1000,
},
removeOnFail: {
age: 30 * 24 * 60 * 60,
count: 100,
},
}
);
darkwatchScanWorker.on('completed', (job, result) => {
console.log(`[DarkWatch:Scan] Job ${job.id} completed:`, result);
});
darkwatchScanWorker.on('failed', (job, err) => {
console.error(`[DarkWatch:Scan] Job ${job?.id} failed:`, err.message);
});
darkwatchScanWorker.on('error', (err) => {
console.error('[DarkWatch:Scan] Worker error:', err.message);
});
export default {
darkwatchScanQueue,
darkwatchScanWorker,
};

View File

@@ -2,3 +2,8 @@ export {
voiceprintAnalysisQueue,
voiceprintAnalysisWorker,
} from './voiceprint.jobs';
export {
darkwatchScanQueue,
darkwatchScanWorker,
} from './darkwatch.jobs';

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -1,5 +1,6 @@
import { Analytics } from '@segment/analytics-node';
import { analyticsEnv, EventType, eventPropertiesSchema } from '../config/analytics.config';
import { hashPhoneNumber } from '../utils/phone-hash';
// Mixpanel service
export class MixpanelService {
@@ -97,7 +98,7 @@ export class MixpanelService {
*/
async spamBlocked(userId: string, phoneNumber: string, confidence: number, method: string): Promise<void> {
await this.track(EventType.SPAM_BLOCKED, userId, {
phoneNumber,
phoneNumber: hashPhoneNumber(phoneNumber),
confidence,
method,
timestamp: new Date(),

View File

@@ -0,0 +1,12 @@
/**
* Hash a phone number for analytics purposes
* Uses a consistent hashing algorithm to create a deterministic hash
*/
export function hashPhoneNumber(phoneNumber: string): string {
let hash = 0;
for (let i = 0; i < phoneNumber.length; i++) {
hash = ((hash << 5) - hash) + phoneNumber.charCodeAt(i);
hash |= 0;
}
return `hash_${Math.abs(hash)}`;
}

View File

@@ -0,0 +1,15 @@
{
"name": "@shieldsai/shared-billing",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"dependencies": {
"stripe": "^14.0.0",
"zod": "^4.3.6"
},
"devDependencies": {
"typescript": "^5.3.3"
}
}

View File

@@ -26,12 +26,13 @@ model User {
accounts Account[]
sessions Session[]
familyGroups FamilyGroupMember[]
subscriptions Subscription[]
watchlist WatchlistItem[]
exposures Exposure[]
alerts Alert[]
familyGroupOwned FamilyGroup[] @relation("FamilyGroupOwner")
subscriptions Subscription[]
alerts Alert[]
voiceEnrollments VoiceEnrollment[]
spamFeedback SpamFeedback[]
voiceAnalyses VoiceAnalysis[]
spamFeedback SpamFeedback[]
spamRules SpamRule[]
// Audit
createdAt DateTime @default(now())

View File

@@ -1,4 +1,4 @@
import { PrismaClient } from './generated/client';
import { PrismaClient } from '@prisma/client';
// Singleton pattern for Prisma Client
const globalForPrisma = globalThis as unknown as {
@@ -45,6 +45,6 @@ export type {
FeedbackType,
RuleType,
RuleAction,
} from './generated/client';
} from '@prisma/client';
export * as PrismaModels from './generated/client';
export * as PrismaModels from '@prisma/client';

View File

@@ -25,6 +25,7 @@ export class EmailService {
subject: string,
htmlBody: string,
textBody?: string,
templateId?: string,
attachments?: Array<{
filename: string;
content: Buffer | string;
@@ -35,7 +36,7 @@ export class EmailService {
id: `email_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
userId: recipient.userId,
channel: NotificationChannel.EMAIL,
templateId: 'custom', // Can be updated to use actual template
templateId: templateId || 'custom',
priority: NotificationPriority.NORMAL,
status: NotificationStatus.PENDING,
to: recipient.email!,

View File

@@ -15,23 +15,27 @@ export class PushService {
constructor(fcmConfig?: FCMConfig, apnsConfig?: APNsConfig) {
if (fcmConfig) {
if (!admin.apps.length && fcmConfig.keyPath) {
// Use named app instance for multi-tenant support
const appName = fcmConfig.keyPath
? `fcm_${fcmConfig.projectId}`
: 'fcm_default';
// Check if app with this name already exists
const existingApp = admin.app(appName);
if (!existingApp) {
this.fcm = admin.initializeApp({
credential: admin.credential.cert({
projectId: fcmConfig.projectId,
privateKey: fcmConfig.privateKey.replace(/\\n/g, '\n'),
clientEmail: fcmConfig.clientEmail,
}),
storageBucket: `${fcmConfig.projectId}.appspot.com`,
});
} else if (!admin.apps.length) {
this.fcm = admin.initializeApp({
credential: admin.credential.cert({
projectId: fcmConfig.projectId,
privateKey: fcmConfig.privateKey.replace(/\\n/g, '\n'),
clientEmail: fcmConfig.clientEmail,
...(fcmConfig.keyPath && {
storageBucket: `${fcmConfig.projectId}.appspot.com`,
}),
});
}, appName);
} else {
this.fcm = existingApp;
}
}
@@ -148,8 +152,9 @@ export class PushService {
return notification;
}
// APNs implementation would go here
// For now, we'll use FCM for iOS as well (FCM supports APNs)
// FCM supports sending to APNs tokens (iOS devices)
// This leverages FCM's unified push infrastructure for iOS
// APNs token format: device-specific token from iOS
if (this.fcm && recipient.apnsToken) {
const message: admin.messaging.Message = {
token: recipient.apnsToken,
@@ -246,12 +251,12 @@ export class PushService {
return !!this.fcm;
}
/**
* Shutdown FCM app
*/
async shutdown(): Promise<void> {
if (this.fcm) {
await this.fcm.terminate();
/**
* Shutdown FCM app
*/
async shutdown(): Promise<void> {
if (this.fcm) {
await this.fcm.delete();
}
}
}
}