Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions __tests__/lib/backup/backup-restore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,19 @@ async function seedTestData(db: mongoose.Connection["db"]) {
}

describe("backup/backup", () => {
let connection: typeof mongoose
let connection: any

beforeEach(async () => {
connection = await mongoose.connect(TEST_DB_URI, { bufferCommands: false })
await seedTestData(connection.db!)
const db = connection.connection?.db || connection.db
await seedTestData(db)
})

afterEach(async () => {
if (connection.db) {
await connection.db.collection("users").deleteMany({})
await connection.db.collection("vehicles").deleteMany({})
const db = connection.connection?.db || connection.db
if (db) {
await db.collection("users").deleteMany({})
await db.collection("vehicles").deleteMany({})
}
await connection.disconnect()
})
Expand Down Expand Up @@ -221,8 +223,8 @@ describe("backup/restore", () => {
})

it("fails with wrong encryption key", async () => {
const conn = await mongoose.connect(TEST_DB_URI, { bufferCommands: false })
await seedTestData(conn.db!)
const conn: any = await mongoose.connect(TEST_DB_URI, { bufferCommands: false })
await seedTestData(conn.connection?.db || conn.db)

const { backupPath } = await performBackup({
backupDir: TEST_BACKUP_DIR,
Expand All @@ -245,8 +247,8 @@ describe("backup/restore", () => {
})

it("dry run reports what would be restored", async () => {
const conn = await mongoose.connect(TEST_DB_URI, { bufferCommands: false })
await seedTestData(conn.db!)
const conn: any = await mongoose.connect(TEST_DB_URI, { bufferCommands: false })
await seedTestData(conn.connection?.db || conn.db)

const { backupPath } = await performBackup({
backupDir: TEST_BACKUP_DIR,
Expand All @@ -270,17 +272,19 @@ describe("backup/restore", () => {
})

describe("backup/verify", () => {
let connection: typeof mongoose
let connection: any

beforeEach(async () => {
connection = await mongoose.connect(TEST_DB_URI, { bufferCommands: false })
await seedTestData(connection.db!)
const db = connection.connection?.db || connection.db
await seedTestData(db)
})

afterEach(async () => {
if (connection.db) {
await connection.db.collection("users").deleteMany({})
await connection.db.collection("vehicles").deleteMany({})
const db = connection.connection?.db || connection.db
if (db) {
await db.collection("users").deleteMany({})
await db.collection("vehicles").deleteMany({})
}
await connection.disconnect()
})
Expand Down
12 changes: 6 additions & 6 deletions __tests__/lib/backup/drill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import mongoose from "mongoose"
import { performBackup } from "@/lib/backup/backup"
import { performRestore } from "@/lib/backup/restore"
import { verifyBackupIntegrity, verifyRestoredDatabase } from "@/lib/backup/verify"
import { generateFixtures, cleanupFixtures } from "../../scripts/backup/generate-fixtures"
import { generateFixtures, cleanupFixtures } from "@/scripts/backup/generate-fixtures"

const TEST_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/chainmove-drill-test"
const DRILL_DIR = join(process.cwd(), "backups", ".drill-test")
Expand All @@ -26,7 +26,7 @@ afterEach(async () => {
describe("restore drill (integration)", () => {
it("generates fixtures, backs up, restores, and verifies", async () => {
const conn = await mongoose.connect(TEST_URI, { bufferCommands: false })
const db = conn.db!
const db = conn.connection.db || (conn as any).db
if (!db) throw new Error("No DB")

try {
Expand Down Expand Up @@ -75,7 +75,7 @@ describe("restore drill (integration)", () => {

it("detects wrong key during restore", async () => {
const conn = await mongoose.connect(TEST_URI, { bufferCommands: false })
const db = conn.db!
const db = (conn as any).connection?.db || (conn as any).db
if (!db) throw new Error("No DB")

try {
Expand Down Expand Up @@ -107,7 +107,7 @@ describe("restore drill (integration)", () => {

it("detects corrupted archive", async () => {
const conn = await mongoose.connect(TEST_URI, { bufferCommands: false })
const db = conn.db!
const db = (conn as any).connection?.db || (conn as any).db
if (!db) throw new Error("No DB")

try {
Expand Down Expand Up @@ -149,7 +149,7 @@ describe("restore drill (integration)", () => {

it("rejects unsafe production target", async () => {
const conn = await mongoose.connect(TEST_URI, { bufferCommands: false })
const db = conn.db!
const db = (conn as any).connection?.db || (conn as any).db
if (!db) throw new Error("No DB")

try {
Expand Down Expand Up @@ -188,7 +188,7 @@ describe("restore drill (integration)", () => {

it("verifies restored database document counts", async () => {
const conn = await mongoose.connect(TEST_URI, { bufferCommands: false })
const db = conn.db!
const db = (conn as any).connection?.db || (conn as any).db
if (!db) throw new Error("No DB")

try {
Expand Down
16 changes: 16 additions & 0 deletions app/api/admin/release/canary/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NextResponse } from "next/server";
import { isUserInCanaryCohort } from "@/lib/release/canary";

export async function POST(request: Request) {
try {
const { userId, canaryPercentage = 10 } = await request.json();
if (!userId) {
return NextResponse.json({ error: "userId is required" }, { status: 400 });
}

const inCohort = isUserInCanaryCohort(userId, canaryPercentage);
return NextResponse.json({ userId, canaryPercentage, inCohort });
} catch (err) {
return NextResponse.json({ error: (err as Error).message }, { status: 500 });
}
}
27 changes: 27 additions & 0 deletions app/api/admin/release/preflight/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextResponse } from "next/server";
import { validateReleaseManifest } from "@/lib/release/manifest";
import { runPreflightChecks } from "@/lib/release/preDeployChecks";

export async function POST(request: Request) {
try {
const body = await request.json();
const manifestValidation = validateReleaseManifest(body);

if (!manifestValidation.valid) {
return NextResponse.json(
{ error: "Invalid release manifest", details: manifestValidation.errors },
{ status: 400 }
);
}

const preflightResult = await runPreflightChecks(body);
return NextResponse.json(preflightResult, {
status: preflightResult.passed ? 200 : 412,
});
} catch (err) {
return NextResponse.json(
{ error: (err as Error).message },
{ status: 500 }
);
}
}
15 changes: 15 additions & 0 deletions app/api/admin/release/rollback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { evaluateRollbackTrigger } from "@/lib/release/canary";

export async function POST(request: Request) {
try {
const { errorRate = 0, probeFailures = 0, maxErrorRatePercentage = 2.0 } = await request.json();
const outcome = evaluateRollbackTrigger(errorRate, maxErrorRatePercentage, probeFailures);

return NextResponse.json({
rollback: outcome,
});
} catch (err) {
return NextResponse.json({ error: (err as Error).message }, { status: 500 });
}
}
17 changes: 9 additions & 8 deletions app/api/kyc-documents/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ export async function GET(request: Request) {

await dbConnect()

const documentOwner = await User.findOne({
kycDocuments: reference,
}).select("_id").lean()

const authContext = await authorizeRequest(request, "kyc:document:read", {
type: "kyc", ownerId: documentOwner?._id?.toString(), exists: Boolean(documentOwner),
})
if ("response" in authContext) return authContext.response

if (query.data.token) {
const verification = verifySignedDocumentUrl(query.data.token)
if (!verification.valid) {
Expand Down Expand Up @@ -72,14 +81,6 @@ export async function GET(request: Request) {
}
}
}
const documentOwner = await User.findOne({
kycDocuments: reference,
}).select("_id").lean()

const authContext = await authorizeRequest(request, "kyc:document:read", {
type: "kyc", ownerId: documentOwner?._id?.toString(), exists: Boolean(documentOwner),
})
if ("response" in authContext) return authContext.response

const rateLimit = consumeRateLimit({
key: buildRateLimitKey("kyc-document", authContext.user._id.toString(), getClientIpAddress(request)),
Expand Down
5 changes: 3 additions & 2 deletions app/api/notifications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import User from "@/models/User"
import { logAuditEvent } from "@/lib/security/audit-log"
import { buildRateLimitKey, consumeRateLimit, getClientIpAddress, rateLimitExceededResponse } from "@/lib/security/rate-limit"
import { decodeCursor, encodeCursor } from "@/lib/api/cursor"
import { ACTIVITY_CATEGORIES, inferActivityCategory } from "@/lib/activity"

const querySchema = z.object({
userId: z.string().trim().regex(/^[a-f\d]{24}$/i, "Invalid userId.").optional(),
Expand Down Expand Up @@ -57,7 +58,7 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "User not found" }, { status: 404 })
}

const notification = await createActivity({
const notification = await Notification.create({
userId: body.data.userId,
createdBy: authContext.user._id.toString(),
title: body.data.title,
Expand All @@ -83,7 +84,7 @@ export async function POST(request: Request) {
actor: authContext.user,
action: "notification.create",
targetType: "user",
targetId: body.data.userId,
targetId: body.data.userId || null,
ipAddress: getClientIpAddress(request),
metadata: {
notificationId: notification._id.toString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect, useMemo, useState } from "react"
import { ExternalLink, Loader2, RefreshCw, Sparkles } from "lucide-react"
// @ts-ignore - date-fns typings bundler resolution
import { format } from "date-fns"

import { Badge } from "@/components/ui/badge"
Expand Down
4 changes: 2 additions & 2 deletions lib/backup/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export async function performBackup(options: BackupOptions): Promise<{
const collectionInfos: CollectionInfo[] = []
let totalSizeBytes = 0

const availableCollections = await db.listCollections()
const availableCollections = await db.listCollections().toArray()
const availableNames = new Set(availableCollections.map((c) => c.name))

for (const collName of collectionsToBackup) {
Expand All @@ -71,7 +71,7 @@ export async function performBackup(options: BackupOptions): Promise<{

const collection = db.collection(collName)
const documents = await collection.find({}).toArray()
const indexes = await getCollectionIndexes(collection)
const indexes = await getCollectionIndexes(collection as any)

const collectionInfo: CollectionInfo = {
name: collName,
Expand Down
2 changes: 1 addition & 1 deletion lib/backup/restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export async function performRestore(options: RestoreOptions): Promise<{
}
}

let restoreConnection: typeof mongoose | null = null
let restoreConnection: mongoose.Connection | null = null

try {
restoreConnection = await mongoose.createConnection(targetUri, {
Expand Down
2 changes: 1 addition & 1 deletion lib/backup/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export async function verifyRestoredDatabase(
collectionResults: {},
}

let connection: typeof mongoose | null = null
let connection: mongoose.Connection | null = null

try {
connection = await mongoose.createConnection(targetUri, {
Expand Down
90 changes: 90 additions & 0 deletions lib/release/canary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createHash } from "crypto";

export interface CanaryConfig {
releaseId: string;
targetPercentage: number;
active: boolean;
probesPassed: number;
probesFailed: number;
}

export function isUserInCanaryCohort(userId: string, targetPercentage: number): boolean {
if (targetPercentage <= 0) return false;
if (targetPercentage >= 100) return true;

const hash = createHash("md5").update(userId).digest("hex");
const num = parseInt(hash.substring(0, 8), 16);
const bucket = num % 100;
return bucket < targetPercentage;
}

export interface FinancialProbe {
operation: string;
run: () => Promise<boolean>;
}

export async function runCanaryProbes(probes: FinancialProbe[]): Promise<{
allPassed: boolean;
passedCount: number;
failedCount: number;
failures: string[];
}> {
let passedCount = 0;
let failedCount = 0;
const failures: string[] = [];

for (const probe of probes) {
try {
const ok = await probe.run();
if (ok) {
passedCount++;
} else {
failedCount++;
failures.push(`Probe for ${probe.operation} failed execution check`);
}
} catch (err) {
failedCount++;
failures.push(`Probe for ${probe.operation} threw: ${(err as Error).message}`);
}
}

return {
allPassed: failedCount === 0,
passedCount,
failedCount,
failures,
};
}

export interface RollbackOutcome {
triggered: boolean;
reason: string;
timestamp: string;
}

export function evaluateRollbackTrigger(
errorRate: number,
maxErrorRatePercentage = 2.0,
probeFailures = 0
): RollbackOutcome {
const timestamp = new Date().toISOString();
if (probeFailures > 0) {
return {
triggered: true,
reason: `Automated rollback triggered due to ${probeFailures} failed financial probes`,
timestamp,
};
}
if (errorRate > maxErrorRatePercentage) {
return {
triggered: true,
reason: `Automated rollback triggered: error rate ${errorRate}% exceeds threshold ${maxErrorRatePercentage}%`,
timestamp,
};
}
return {
triggered: false,
reason: "Metrics healthy. Rollback not required.",
timestamp,
};
}
Loading
Loading