Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e6f7884
fix(email): preserve DSN failure classification during reconciliation…
kjxcodez Sep 9, 2026
c8fb577
Merge pull request #40 from kjxcodez/fix/email-preserve-dsn-failure-c…
kjxcodez Sep 9, 2026
3e8b136
feat(campaigns): add outbound rejection circuit breaker (#35)
kjxcodez Sep 9, 2026
6d224b6
Merge pull request #41 from kjxcodez/feat/campaigns/outbound-rejectio…
kjxcodez Sep 9, 2026
de48bac
feat(outreach): enforce domain pacing and company contact limits (#36)
kjxcodez Sep 9, 2026
3ce634a
Merge pull request #42 from kjxcodez/feat/outreach/domain-pacing-card…
kjxcodez Sep 10, 2026
bc107c5
fix(email): prevent ambiguous delivery blind re-dispatch (#37)
kjxcodez Sep 10, 2026
5343c0d
Merge pull request #43 from kjxcodez/fix/email-prevent-ambiguous-redi…
kjxcodez Sep 10, 2026
a208b78
fix(outreach): enforce company dnc and domain suppression cascade (#38)
kjxcodez Sep 10, 2026
b197ede
Merge pull request #44 from kjxcodez/fix/outreach/company-dnc-domain-…
kjxcodez Sep 11, 2026
0acdece
chore: cleanup repository archaeology and establish canonical baseline
kjxcodez Sep 11, 2026
e0a4598
feat: add generated contributors data for marketing app
kjxcodez Sep 11, 2026
dbf8357
Merge pull request #45 from kjxcodez/chore/repository-cleanup-baseline
kjxcodez Sep 11, 2026
2022c92
docs: add current-state release audit report detailing system readine…
kjxcodez Sep 11, 2026
fff9700
Merge pull request #46 from kjxcodez/audit/current-state-release-audit
kjxcodez Sep 11, 2026
cb241f0
feat(email): make campaign tracking opt-in and safe
kjxcodez Sep 11, 2026
8919c42
feat(scheduler): persist concurrency policy in mongodb
kjxcodez Sep 11, 2026
a1e8936
feat(scheduler): prevent discovery from starving outreach
kjxcodez Sep 11, 2026
43e5490
fix(ipc): repair ipc channel contracts
kjxcodez Sep 11, 2026
921bb54
fix(contacts): make paginated selection id-safe
kjxcodez Sep 11, 2026
565afe2
feat(contacts): support select all matching records
kjxcodez Sep 12, 2026
00ae7b7
fix(sync): reconcile projections and add global refresh
kjxcodez Sep 12, 2026
d413051
feat(discovery): add safe discovery run deletion
kjxcodez Sep 15, 2026
e8676f9
feat(company): implement safe company deletion semantics
kjxcodez Sep 15, 2026
9ed1d59
feat(email): implement efficient imap polling with server-side range …
kjxcodez Sep 15, 2026
4a44c82
feat(email): implement email threading and message semantics
kjxcodez Sep 15, 2026
28bdba0
feat(discovery): replace native datalist with accessible geography se…
kjxcodez Sep 15, 2026
181df11
feat(email): improve email logs filter discoverability and responsive…
kjxcodez Sep 15, 2026
6fb19ab
feat: add workspace runtime management, discovery services, API route…
kjxcodez Sep 16, 2026
6819759
fix(qualification): decouple isMailboxEligibleForDispatch from wall c…
kjxcodez Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ yarn-error.log*
.DS_Store
stitch_leadforge_dashboard_design_system

# TS build info cache
# TS build info cache and source maps
*.tsbuildinfo
*.map

# LeadForge OS Beta telemetry, logs, and local databases
report/*
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ graph TD
MP <-->|SyncEngine SdkClient| CN[Cloud Hono Server - MongoDB]
```

For a detailed breakdown of process lifecycles, data flows, and schemas, view the [System Architecture Guide](file:///c:/Users/91637/Desktop/Business%20Project/leadforge-os/docs/architecture/README.md).
For a detailed breakdown of process lifecycles, data flows, and schemas, view the [System Architecture Guide](docs/architecture/current-architecture.md).

---

Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/db/models/campaign.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface CampaignDocument
schedule?: Record<string, any> | string | null;
timezone: string;
dailyLimit: number;
trackingEnabled?: boolean | null;
settings?: Record<string, any> | null;
idempotencyKey?: string | null;
}
Expand Down Expand Up @@ -89,6 +90,10 @@ const campaignSchema = new Schema<CampaignDocument>(
type: Number,
default: 0
},
trackingEnabled: {
type: Boolean,
default: false
},
settings: {
type: Schema.Types.Mixed,
default: null
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/db/models/email-delivery.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ export interface EmailDeliveryDocument
accountId: string;
senderEmail: string;
recipientEmail: string;
recipientDomain?: string | null;
subject: string;
htmlBody?: string | null;
textBody?: string | null;
attachments?: EmailAttachmentDoc[];
provider?: string;
messageId?: string | null;
providerMessageId?: string | null;
providerThreadId?: string | null;
status: EmailDeliveryStatus;
Expand Down Expand Up @@ -104,6 +106,7 @@ const emailDeliverySchema = new Schema<EmailDeliveryDocument>(
accountId: { type: String, required: true, index: true },
senderEmail: { type: String, required: true, lowercase: true, trim: true },
recipientEmail: { type: String, required: true, lowercase: true, trim: true },
recipientDomain: { type: String, default: null, lowercase: true, trim: true, index: true },
subject: { type: String, required: true },
htmlBody: { type: String, default: null },
textBody: { type: String, default: null },
Expand All @@ -122,6 +125,7 @@ const emailDeliverySchema = new Schema<EmailDeliveryDocument>(
}
],
provider: { type: String, default: 'gmail' },
messageId: { type: String, default: null, index: true },
providerMessageId: { type: String, default: null, index: true },
providerThreadId: { type: String, default: null, index: true },
status: {
Expand Down Expand Up @@ -209,6 +213,8 @@ emailDeliverySchema.index({ 'clickTrackingTokens.token': 1 }, { sparse: true });
// 5. Thread & direction indexes for rapid reply correlation and message logs:
emailDeliverySchema.index({ workspaceId: 1, providerThreadId: 1 });
emailDeliverySchema.index({ workspaceId: 1, direction: 1, createdAt: -1 });
emailDeliverySchema.index({ workspaceId: 1, recipientDomain: 1, createdAt: -1 });
emailDeliverySchema.index({ workspaceId: 1, campaignId: 1, companyId: 1, createdAt: -1 });
emailDeliverySchema.index({ workspaceId: 1, status: 1, reconciliationLeaseExpiresAt: 1 });

// Note: Permanent outbound send ledger; zero TTL index.
Expand Down
24 changes: 20 additions & 4 deletions apps/api/src/db/models/suppression.model.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import mongoose, { Schema } from 'mongoose';
import { workspacePlugin, type WorkspaceScopedDocument } from '../plugins/index.js';
import { SuppressionReason } from '@leadforge/schema';
import { SuppressionReason, SuppressionTargetType } from '@leadforge/schema';

export interface SuppressionDocument extends mongoose.Document, WorkspaceScopedDocument {
email: string;
targetType: SuppressionTargetType;
targetId: string;
email?: string | null;
companyId?: string | null;
domain?: string | null;
reason: SuppressionReason;
source: string;
evidence?: Record<string, any> | null;
Expand All @@ -17,7 +21,16 @@ export interface SuppressionDocument extends mongoose.Document, WorkspaceScopedD
const suppressionSchema = new Schema<SuppressionDocument>(
{
workspaceId: { type: String, required: true, index: true },
email: { type: String, required: true, trim: true, lowercase: true },
targetType: {
type: String,
enum: Object.values(SuppressionTargetType),
default: SuppressionTargetType.RECIPIENT,
required: true
},
targetId: { type: String, required: true, trim: true },
email: { type: String, default: null, trim: true, lowercase: true },
companyId: { type: String, default: null, trim: true },
domain: { type: String, default: null, trim: true, lowercase: true },
reason: {
type: String,
enum: Object.values(SuppressionReason),
Expand All @@ -36,7 +49,10 @@ const suppressionSchema = new Schema<SuppressionDocument>(
);

suppressionSchema.plugin(workspacePlugin);
suppressionSchema.index({ workspaceId: 1, email: 1 }, { unique: true });
suppressionSchema.index({ workspaceId: 1, targetType: 1, targetId: 1 }, { unique: true });
suppressionSchema.index({ workspaceId: 1, targetType: 1, companyId: 1 }, { sparse: true });
suppressionSchema.index({ workspaceId: 1, targetType: 1, domain: 1 }, { sparse: true });
suppressionSchema.index({ workspaceId: 1, email: 1 }, { sparse: true });
suppressionSchema.index({ workspaceId: 1, reason: 1 });

export const SuppressionModel =
Expand Down
32 changes: 31 additions & 1 deletion apps/api/src/db/models/workspace.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import mongoose, { Schema } from 'mongoose';
import { generateEntityId } from '@leadforge/schema';
import { generateEntityId, DEFAULT_SCHEDULER_POLICY } from '@leadforge/schema';
import {
softDeletePlugin,
auditPlugin,
Expand Down Expand Up @@ -35,6 +35,11 @@ export interface WorkspaceDocument
hourlyLimit?: number | null;
minSendIntervalMs?: number | null;
} | null;
schedulerPolicy?: {
globalMaxConcurrency: number;
typeLimits: Record<string, number>;
updatedAt?: Date;
} | null;
};
members: WorkspaceMember[];
billing?: Record<string, any> | null;
Expand Down Expand Up @@ -89,6 +94,31 @@ const workspaceSchema = new Schema<WorkspaceDocument>(
{ _id: false }
),
default: null
},
schedulerPolicy: {
type: new Schema(
{
globalMaxConcurrency: {
type: Number,
required: true,
default: () => DEFAULT_SCHEDULER_POLICY.globalMaxConcurrency
},
typeLimits: {
type: Schema.Types.Mixed,
default: () => ({ ...DEFAULT_SCHEDULER_POLICY.typeLimits })
},
updatedAt: {
type: Date,
default: Date.now
}
},
{ _id: false }
),
default: () => ({
globalMaxConcurrency: DEFAULT_SCHEDULER_POLICY.globalMaxConcurrency,
typeLimits: { ...DEFAULT_SCHEDULER_POLICY.typeLimits },
updatedAt: new Date()
})
}
},
members: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type ClientSession } from 'mongoose';
import { BaseRepository } from '../base/base.repository.js';
import {
CompanyDiscoveryRunModel,
Expand All @@ -8,4 +9,24 @@ export class CompanyDiscoveryRunRepository extends BaseRepository<CompanyDiscove
constructor(workspaceId?: string) {
super(CompanyDiscoveryRunModel, workspaceId);
}

/**
* Hard-deletes all provenance junction records linking companies to a specific discovery run.
* Strictly isolated to the active workspaceId.
*/
public async deleteForRun(discoveryRunId: string, session?: ClientSession): Promise<number> {
const filter = this.applyScope({ discoveryRunId });
const result = await this.model.deleteMany(filter).session(session || null);
return result.deletedCount || 0;
}

/**
* Hard-deletes all provenance junction records linking a specific company.
* Strictly isolated to the active workspaceId.
*/
public async deleteForCompany(companyId: string, session?: ClientSession): Promise<number> {
const filter = this.applyScope({ companyId });
const result = await this.model.deleteMany(filter).session(session || null);
return result.deletedCount || 0;
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { BaseRepository } from '../base/base.repository.js';
import { EmailDeliveryModel, type EmailDeliveryDocument } from '../../db/models/email-delivery.model.js';
import type { EmailDeliveryStatus, ReserveEmailDeliveryDto } from '@leadforge/schema';
import { generateEntityId } from '@leadforge/schema';
import { generateEntityId, normalizeDomain } from '@leadforge/schema';
import { EmailDomainError } from '../../services/email/types.js';

export const VALID_DELIVERY_TRANSITIONS: Record<EmailDeliveryStatus, EmailDeliveryStatus[]> = {
QUEUED: ['SENDING', 'SENT', 'FAILED', 'CANCELLED', 'SUPPRESSED'],
SENDING: ['SENT', 'FAILED', 'RETRYING', 'AMBIGUOUS', 'CANCELLED'],
RETRYING: ['SENDING', 'SENT', 'CANCELLED', 'FAILED'],
AMBIGUOUS: ['SENT', 'FAILED', 'RETRYING', 'CANCELLED', 'SENDING'],
FAILED: ['SENDING', 'RETRYING'], // Allow retry on failed deliveries
AMBIGUOUS: ['SENT', 'FAILED', 'CANCELLED'], // Strictly non-retryable; requires reconciliation
FAILED: ['SENDING', 'RETRYING'], // Allow retry on retryable failed deliveries
SENT: [], // Terminal
CANCELLED: ['QUEUED', 'SENDING'],
SUPPRESSED: [], // Terminal
Expand Down Expand Up @@ -56,6 +56,24 @@ export class EmailDeliveryRepository extends BaseRepository<EmailDeliveryDocumen
return { delivery: existing, isAlreadySent: true };
}

// Invariant: An AMBIGUOUS delivery must never be automatically re-dispatched.
if (existing.status === 'AMBIGUOUS') {
throw new EmailDomainError(
'AMBIGUOUS_SEND_TIMEOUT',
`Delivery with idempotency key "${dto.idempotencyKey}" is in AMBIGUOUS state pending reconciliation. Blind re-dispatch is forbidden.`,
false,
false
);
}

// If existing failed delivery was permanent (non-retryable), forbid re-sending
if (existing.status === 'FAILED' && existing.retryable === false) {
throw new EmailDomainError(
'EMAIL_SEND_FAILED',
`Cannot transition delivery ${existing._id} from permanent FAILED status to SENDING.`
);
}

// If already in active SENDING state with valid lease, prevent concurrent duplicate execution
if (
existing.status === 'SENDING' &&
Expand All @@ -78,6 +96,8 @@ export class EmailDeliveryRepository extends BaseRepository<EmailDeliveryDocumen
);
}

const recipientDomain = (dto as any).recipientDomain || normalizeDomain(dto.recipientEmail);

// Reclaim / transition to SENDING
const updated = await this.atomicFindOneAndUpdate(
{ _id: existing._id },
Expand All @@ -87,9 +107,13 @@ export class EmailDeliveryRepository extends BaseRepository<EmailDeliveryDocumen
leaseExpiresAt,
senderEmail: dto.senderEmail,
recipientEmail: dto.recipientEmail,
recipientDomain,
subject: dto.subject,
htmlBody: dto.htmlBody || existing.htmlBody,
textBody: dto.textBody || existing.textBody,
messageId: dto.messageId !== undefined ? dto.messageId : existing.messageId,
inReplyTo: dto.inReplyTo !== undefined ? dto.inReplyTo : existing.inReplyTo,
references: dto.references !== undefined ? dto.references : existing.references,
attachments: (dto.attachments as any) || existing.attachments,
openTrackingToken: existing.openTrackingToken || dto.openTrackingToken,
clickTrackingTokens: existing.clickTrackingTokens?.length ? existing.clickTrackingTokens : ((dto.clickTrackingTokens as any) || []),
Expand All @@ -107,8 +131,46 @@ export class EmailDeliveryRepository extends BaseRepository<EmailDeliveryDocumen
return { delivery: updated!, isAlreadySent: false };
}

// Invariant: Prevent creating a duplicate delivery record for an execution/contact/step with an existing AMBIGUOUS delivery
if (dto.executionId && dto.contactId && dto.stepIndex !== undefined) {
const ambiguousExecution = await this.findOne({
executionId: dto.executionId,
contactId: dto.contactId,
stepIndex: dto.stepIndex,
status: 'AMBIGUOUS'
});

if (ambiguousExecution) {
throw new EmailDomainError(
'AMBIGUOUS_SEND_TIMEOUT',
`An outbound delivery for execution "${dto.executionId}", step ${dto.stepIndex}, contact "${dto.contactId}" is in AMBIGUOUS state pending reconciliation. Blind re-dispatch is forbidden.`,
false,
false
);
}
}

if (dto.campaignId && dto.contactId && dto.stepIndex !== undefined) {
const ambiguousCampaign = await this.findOne({
campaignId: dto.campaignId,
contactId: dto.contactId,
stepIndex: dto.stepIndex,
status: 'AMBIGUOUS'
});

if (ambiguousCampaign) {
throw new EmailDomainError(
'AMBIGUOUS_SEND_TIMEOUT',
`An outbound delivery for campaign "${dto.campaignId}", step ${dto.stepIndex}, contact "${dto.contactId}" is in AMBIGUOUS state pending reconciliation. Blind re-dispatch is forbidden.`,
false,
false
);
}
}

// Create fresh delivery in SENDING state
try {
const recipientDomain = (dto as any).recipientDomain || normalizeDomain(dto.recipientEmail);
const created = await this.create({
_id: dto.id || generateEntityId(),
workspaceId: wsId,
Expand All @@ -121,9 +183,13 @@ export class EmailDeliveryRepository extends BaseRepository<EmailDeliveryDocumen
accountId: dto.accountId,
senderEmail: dto.senderEmail,
recipientEmail: dto.recipientEmail,
recipientDomain,
subject: dto.subject,
htmlBody: dto.htmlBody || null,
textBody: dto.textBody || null,
messageId: dto.messageId || null,
inReplyTo: dto.inReplyTo || null,
references: dto.references || [],
templateId: dto.templateId || null,
templateVersion: dto.templateVersion || null,
variablesSnapshot: dto.variablesSnapshot || null,
Expand All @@ -148,6 +214,14 @@ export class EmailDeliveryRepository extends BaseRepository<EmailDeliveryDocumen
if (concurrentDoc && (concurrentDoc.status === 'SENT' || concurrentDoc.status === 'SUPPRESSED')) {
return { delivery: concurrentDoc, isAlreadySent: true };
}
if (concurrentDoc && concurrentDoc.status === 'AMBIGUOUS') {
throw new EmailDomainError(
'AMBIGUOUS_SEND_TIMEOUT',
`Delivery with idempotency key "${dto.idempotencyKey}" is in AMBIGUOUS state pending reconciliation. Blind re-dispatch is forbidden.`,
false,
false
);
}
throw new EmailDomainError(
'DELIVERY_ALREADY_RESERVED',
`Concurrent delivery creation conflict for idempotency key ${dto.idempotencyKey}.`,
Expand All @@ -164,7 +238,14 @@ export class EmailDeliveryRepository extends BaseRepository<EmailDeliveryDocumen
*/
public async finalizeDelivery(
id: string,
result: { providerMessageId: string; providerThreadId?: string | null | undefined; sentAt?: Date | undefined }
result: {
providerMessageId: string;
providerThreadId?: string | null | undefined;
messageId?: string | null | undefined;
inReplyTo?: string | null | undefined;
references?: string[] | undefined;
sentAt?: Date | undefined;
}
): Promise<EmailDeliveryDocument> {
const existing = await this.findById(id);
if (!existing) {
Expand All @@ -185,6 +266,9 @@ export class EmailDeliveryRepository extends BaseRepository<EmailDeliveryDocumen
status: 'SENT',
providerMessageId: result.providerMessageId,
providerThreadId: result.providerThreadId || null,
...(result.messageId !== undefined ? { messageId: result.messageId } : {}),
...(result.inReplyTo !== undefined ? { inReplyTo: result.inReplyTo } : {}),
...(result.references !== undefined ? { references: result.references } : {}),
sentAt: result.sentAt || new Date(),
leaseExpiresAt: null,
error: null,
Expand Down
Loading
Loading